From e14d854bf9e883a7f50e4a68d442d7af034ce4a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Santos?= Date: Mon, 3 Nov 2025 18:04:28 +0000 Subject: [PATCH] Show article images --- composeApp/build.gradle.kts | 1 + .../ooni/probe/data/models/ArticleModel.kt | 3 +- .../data/repositories/ArticleRepository.kt | 6 +- .../ooni/probe/domain/articles/GetFindings.kt | 1 + .../ooni/probe/domain/articles/GetRSSFeed.kt | 16 +- .../org/ooni/probe/ui/articles/ArticleCard.kt | 168 ++++++++++++++++++ .../org/ooni/probe/ui/articles/ArticleCell.kt | 90 ---------- .../ooni/probe/ui/articles/ArticlesScreen.kt | 2 +- .../probe/ui/dashboard/DashboardScreen.kt | 4 +- .../commonMain/sqldelight/migrations/15.sqm | 1 + .../sqldelight/org/ooni/probe/data/Article.sq | 6 +- .../probe/domain/articles/GetRssFeedTest.kt | 42 ++++- .../testing/factories/ArticleModelFactory.kt | 4 +- gradle/libs.versions.toml | 18 +- 14 files changed, 255 insertions(+), 107 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/org/ooni/probe/ui/articles/ArticleCard.kt delete mode 100644 composeApp/src/commonMain/kotlin/org/ooni/probe/ui/articles/ArticleCell.kt diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index ffb177cb8..fcd9256a8 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -99,6 +99,7 @@ kotlin { iosMain.dependencies { implementation(libs.sqldelight.native) implementation(libs.bundles.mobile) + implementation(libs.bundles.ios) } val desktopMain by getting { dependencies { diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/ArticleModel.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/ArticleModel.kt index 318e5dbcf..d68f9d9a2 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/ArticleModel.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/ArticleModel.kt @@ -9,8 +9,9 @@ import org.ooni.probe.shared.today data class ArticleModel( val url: Url, val title: String, - val description: String?, val source: Source, + val description: String?, + val imageUrl: String?, val time: LocalDateTime, ) { data class Url( diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/ArticleRepository.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/ArticleRepository.kt index 3e7043380..e1824863f 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/ArticleRepository.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/ArticleRepository.kt @@ -23,8 +23,9 @@ class ArticleRepository( database.articleQueries.insertOrReplace( url = model.url.value, title = model.title, - description = model.description, source = model.source.value, + description = model.description, + image_url = model.imageUrl, time = model.time.toEpoch(), ) } @@ -45,8 +46,9 @@ class ArticleRepository( ArticleModel( url = ArticleModel.Url(url), title = title, - description = description, source = ArticleModel.Source.fromValue(source) ?: return@run null, + description = description, + imageUrl = image_url, time = time.toLocalDateTime(), ) } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/articles/GetFindings.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/articles/GetFindings.kt index 5c30c5e6b..34ee184ad 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/articles/GetFindings.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/articles/GetFindings.kt @@ -46,6 +46,7 @@ class GetFindings( title = title ?: return@run null, source = ArticleModel.Source.Finding, description = shortDescription, + imageUrl = null, time = createTime?.toLocalDateTime() ?: return@run null, ) } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/articles/GetRSSFeed.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/articles/GetRSSFeed.kt index e68857b87..9f5a0bbc1 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/articles/GetRSSFeed.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/articles/GetRSSFeed.kt @@ -9,8 +9,6 @@ import kotlinx.datetime.format.byUnicodePattern import kotlinx.datetime.parse import kotlinx.serialization.Serializable import kotlinx.serialization.decodeFromString -import nl.adaptivity.xmlutil.ExperimentalXmlUtilApi -import nl.adaptivity.xmlutil.serialization.UnknownChildHandler import nl.adaptivity.xmlutil.serialization.XML import nl.adaptivity.xmlutil.serialization.XmlElement import nl.adaptivity.xmlutil.serialization.XmlSerialName @@ -56,6 +54,7 @@ class GetRSSFeed( title = title ?: return@run null, source = source, description = description, + imageUrl = content?.url, time = pubDate?.toLocalDateTime() ?: return@run null, ) } @@ -80,8 +79,7 @@ class GetRSSFeed( private val Xml by lazy { XML { defaultPolicy { - @OptIn(ExperimentalXmlUtilApi::class) - unknownChildHandler = UnknownChildHandler { _, _, _, _, _ -> emptyList() } + ignoreUnknownChildren() } } } @@ -115,6 +113,16 @@ class GetRSSFeed( @XmlSerialName("pubDate") @XmlElement val pubDate: String?, + @XmlSerialName("content", namespace = "http://search.yahoo.com/mrss/") + @XmlElement + val content: MediaContent?, + ) + + @Serializable + data class MediaContent( + @XmlSerialName("url") + @XmlElement(false) + val url: String?, ) } } diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/articles/ArticleCard.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/articles/ArticleCard.kt new file mode 100644 index 000000000..77bb949f1 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/articles/ArticleCard.kt @@ -0,0 +1,168 @@ +package org.ooni.probe.ui.articles + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil3.compose.AsyncImagePainter +import coil3.compose.rememberAsyncImagePainter +import kotlinx.datetime.LocalDate +import ooniprobe.composeapp.generated.resources.Dashboard_Articles_Blog +import ooniprobe.composeapp.generated.resources.Dashboard_Articles_Finding +import ooniprobe.composeapp.generated.resources.Dashboard_Articles_Recent +import ooniprobe.composeapp.generated.resources.Dashboard_Articles_Report +import ooniprobe.composeapp.generated.resources.Res +import ooniprobe.composeapp.generated.resources.ic_cloud_off +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview +import org.ooni.probe.data.models.ArticleModel +import org.ooni.probe.shared.toDateTime +import org.ooni.probe.ui.shared.articleFormat +import org.ooni.probe.ui.theme.AppTheme +import org.ooni.probe.ui.theme.LocalCustomColors + +@Composable +fun ArticleCard( + article: ArticleModel, + onClick: () -> Unit, +) { + OutlinedCard( + onClick = onClick, + colors = CardDefaults + .outlinedCardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerLowest), + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(bottom = 8.dp), + ) { + Row( + Modifier + .height(IntrinsicSize.Min) + .defaultMinSize(minHeight = 88.dp), + ) { + Column(Modifier.weight(2f)) { + Text( + article.title, + style = MaterialTheme.typography.titleMedium, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .padding(horizontal = 12.dp) + .padding(top = 8.dp, bottom = 4.dp), + ) + Text( + buildAnnotatedString { + withStyle( + SpanStyle( + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Bold, + ), + ) { + append( + stringResource( + when (article.source) { + ArticleModel.Source.Blog -> Res.string.Dashboard_Articles_Blog + ArticleModel.Source.Finding -> Res.string.Dashboard_Articles_Finding + ArticleModel.Source.Report -> Res.string.Dashboard_Articles_Report + }, + ), + ) + } + append(" • ") + append(article.time.articleFormat()) + if (article.isRecent) { + append(" • ") + withStyle(SpanStyle(color = LocalCustomColors.current.success)) { + append(stringResource(Res.string.Dashboard_Articles_Recent)) + } + } + }, + style = MaterialTheme.typography.labelLarge, + modifier = Modifier.padding(horizontal = 12.dp).padding(bottom = 8.dp), + ) + } + article.imageUrl?.let { imageUrl -> + val painter = rememberAsyncImagePainter(imageUrl) + val state by painter.state.collectAsStateWithLifecycle() + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .fillMaxHeight() + .height(IntrinsicSize.Min) + .clip(MaterialTheme.shapes.medium) + .background(MaterialTheme.colorScheme.surfaceVariant) + .border( + Dp.Hairline, + MaterialTheme.colorScheme.onSurface.copy(alpha = 0.25f), + MaterialTheme.shapes.medium, + ).weight(1f), + ) { + Image( + painter = painter, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.matchParentSize(), + ) + if (state is AsyncImagePainter.State.Loading) { + CircularProgressIndicator( + Modifier.size(48.dp), + ) + } + if (state is AsyncImagePainter.State.Error) { + Image( + painterResource(Res.drawable.ic_cloud_off), + contentDescription = null, + modifier = Modifier.alpha(0.5f), + ) + } + } + } + } + } +} + +@Composable +@Preview +fun ArticleCellPreview() { + AppTheme { + ArticleCard( + article = ArticleModel( + url = ArticleModel.Url("http://ooni.org"), + title = "Join us at the OMG Village at the Global Gathering 2025!", + description = "Hello there.", + imageUrl = "https://ooni.org/images/logos/OONI-VerticalColor@2x.png", + source = ArticleModel.Source.Blog, + time = LocalDate(2025, 4, 1).toDateTime(), + ), + onClick = {}, + ) + } +} diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/articles/ArticleCell.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/articles/ArticleCell.kt deleted file mode 100644 index 26786b373..000000000 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/articles/ArticleCell.kt +++ /dev/null @@ -1,90 +0,0 @@ -package org.ooni.probe.ui.articles - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedCard -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import ooniprobe.composeapp.generated.resources.Dashboard_Articles_Blog -import ooniprobe.composeapp.generated.resources.Dashboard_Articles_Finding -import ooniprobe.composeapp.generated.resources.Dashboard_Articles_Recent -import ooniprobe.composeapp.generated.resources.Dashboard_Articles_Report -import ooniprobe.composeapp.generated.resources.Res -import org.jetbrains.compose.resources.stringResource -import org.ooni.probe.data.models.ArticleModel -import org.ooni.probe.data.models.ArticleModel.Source.Blog -import org.ooni.probe.data.models.ArticleModel.Source.Finding -import org.ooni.probe.data.models.ArticleModel.Source.Report -import org.ooni.probe.ui.shared.articleFormat -import org.ooni.probe.ui.theme.LocalCustomColors - -@Composable -fun ArticleCell( - article: ArticleModel, - onClick: () -> Unit, -) { - OutlinedCard( - onClick = onClick, - modifier = Modifier.padding(horizontal = 16.dp).padding(bottom = 8.dp), - ) { - Column( - Modifier - .fillMaxWidth() - .padding(horizontal = 12.dp, vertical = 8.dp), - ) { - Text( - article.title, - style = MaterialTheme.typography.titleMedium, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) - Row(verticalAlignment = Alignment.Bottom) { - Text( - stringResource( - when (article.source) { - Blog -> Res.string.Dashboard_Articles_Blog - Finding -> Res.string.Dashboard_Articles_Finding - Report -> Res.string.Dashboard_Articles_Report - }, - ), - style = MaterialTheme.typography.labelLarge - .copy(fontWeight = FontWeight.Bold), - color = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(top = 4.dp), - ) - - Text( - "•", - style = MaterialTheme.typography.labelLarge, - modifier = Modifier.padding(horizontal = 4.dp), - ) - Text( - article.time.articleFormat(), - style = MaterialTheme.typography.labelLarge, - modifier = Modifier.padding(top = 4.dp), - ) - if (article.isRecent) { - Text( - "•", - style = MaterialTheme.typography.labelLarge, - modifier = Modifier.padding(horizontal = 4.dp), - ) - Text( - stringResource(Res.string.Dashboard_Articles_Recent), - style = MaterialTheme.typography.labelLarge, - color = LocalCustomColors.current.success, - modifier = Modifier.padding(top = 4.dp), - ) - } - } - } - } -} diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/articles/ArticlesScreen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/articles/ArticlesScreen.kt index 764dd638f..f30054ff7 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/articles/ArticlesScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/articles/ArticlesScreen.kt @@ -78,7 +78,7 @@ fun ArticlesScreen( state = lazyListState, ) { items(state.articles, key = { it.url.value }) { article -> - ArticleCell( + ArticleCard( article = article, onClick = { onEvent(ArticlesViewModel.Event.ArticleClicked(article)) }, ) diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/DashboardScreen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/DashboardScreen.kt index 61fdfbb47..40c63f98d 100644 --- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/DashboardScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/dashboard/DashboardScreen.kt @@ -87,7 +87,7 @@ import org.ooni.probe.data.models.MeasurementStats import org.ooni.probe.data.models.Run import org.ooni.probe.data.models.RunBackgroundState import org.ooni.probe.shared.largeNumberShort -import org.ooni.probe.ui.articles.ArticleCell +import org.ooni.probe.ui.articles.ArticleCard import org.ooni.probe.ui.shared.IgnoreBatteryOptimizationDialog import org.ooni.probe.ui.shared.TestRunErrorMessages import org.ooni.probe.ui.shared.VerticalScrollbar @@ -486,7 +486,7 @@ private fun ArticlesSection( ) articles.forEach { article -> - ArticleCell( + ArticleCard( article = article, onClick = { onEvent(DashboardViewModel.Event.ArticleClicked(article)) }, ) diff --git a/composeApp/src/commonMain/sqldelight/migrations/15.sqm b/composeApp/src/commonMain/sqldelight/migrations/15.sqm index 881eb05b4..89cb5373f 100644 --- a/composeApp/src/commonMain/sqldelight/migrations/15.sqm +++ b/composeApp/src/commonMain/sqldelight/migrations/15.sqm @@ -3,5 +3,6 @@ CREATE TABLE Article( title TEXT NOT NULL, source TEXT NOT NULL, description TEXT, + image_url TEXT, time INTEGER NOT NULL ); diff --git a/composeApp/src/commonMain/sqldelight/org/ooni/probe/data/Article.sq b/composeApp/src/commonMain/sqldelight/org/ooni/probe/data/Article.sq index 357b64b91..5b799cf4b 100644 --- a/composeApp/src/commonMain/sqldelight/org/ooni/probe/data/Article.sq +++ b/composeApp/src/commonMain/sqldelight/org/ooni/probe/data/Article.sq @@ -3,6 +3,7 @@ CREATE TABLE Article( title TEXT NOT NULL, source TEXT NOT NULL, description TEXT, + image_url TEXT, time INTEGER NOT NULL ); @@ -10,10 +11,11 @@ insertOrReplace: INSERT OR REPLACE INTO Article ( url, title, - description, source, + description, + image_url, time -) VALUES (?,?,?,?,?); +) VALUES (?,?,?,?,?,?); selectAll: SELECT * FROM Article ORDER BY time DESC; diff --git a/composeApp/src/commonTest/kotlin/org/ooni/probe/domain/articles/GetRssFeedTest.kt b/composeApp/src/commonTest/kotlin/org/ooni/probe/domain/articles/GetRssFeedTest.kt index 498352a2a..011ef8c79 100644 --- a/composeApp/src/commonTest/kotlin/org/ooni/probe/domain/articles/GetRssFeedTest.kt +++ b/composeApp/src/commonTest/kotlin/org/ooni/probe/domain/articles/GetRssFeedTest.kt @@ -22,11 +22,49 @@ class GetRssFeedTest { assertEquals("https://ooni.org/post/2025-gg-omg-village/", url.value) assertEquals("Join us at the OMG Village at the Global Gathering 2025!", title) assertEquals(2025, time.year) + assertEquals("https://ooni.org/post/2025-gg-omg-village/images/omg-banner.png", imageUrl) } } companion object { - private const val RSS_FEED = - "Blog posts on OONI: Open Observatory of Network Interferencehttps://ooni.org/blog/Recent content in Blog posts on OONI: Open Observatory of Network InterferenceHugoenJoin us at the OMG Village at the Global Gathering 2025!https://ooni.org/post/2025-gg-omg-village/Mon, 01 Sep 2025 00:00:00 +0000https://ooni.org/post/2025-gg-omg-village/<p>Are you attending the upcoming <a href=\"https://wiki.digitalrights.community/index.php?title=Global_Gathering_2025\">Global Gathering</a> event in Estoril, Portugal? Are you interested in investigating internet shutdowns and censorship, and curious to learn more about the tools and open datasets that support this work?</p>" + private val RSS_FEED = """ + + + + Blog posts on OONI: Open Observatory of Network Interference + https://ooni.org/blog/ + Recent content in Blog posts on OONI: Open Observatory of Network Interference + Hugo + en + + + Join us at the OMG Village at the Global Gathering 2025! + https://ooni.org/post/2025-gg-omg-village/ + + Mon, 01 Sep 2025 00:00:00 +0000 + https://ooni.org/post/2025-gg-omg-village/ + + <div> + <a href="https://ooni.org/post/2025-gg-omg-village/images/omg-banner.png"> + <img + src="https://ooni.org/post/2025-gg-omg-village/images/omg-banner_hu_1e6c0cc0ca63d2d9.png" + + + srcset="https://ooni.org/post/2025-gg-omg-village/images/omg-banner_hu_79aea09410c8ceb7.png 2x" + + + title="OMG Village announcement" + + alt="OMG Village announcement" + + /> + </a> + + </div> + + + + + """.trimIndent() } } diff --git a/composeApp/src/commonTest/kotlin/org/ooni/testing/factories/ArticleModelFactory.kt b/composeApp/src/commonTest/kotlin/org/ooni/testing/factories/ArticleModelFactory.kt index 5e5d607a0..d1076d593 100644 --- a/composeApp/src/commonTest/kotlin/org/ooni/testing/factories/ArticleModelFactory.kt +++ b/composeApp/src/commonTest/kotlin/org/ooni/testing/factories/ArticleModelFactory.kt @@ -11,13 +11,15 @@ object ArticleModelFactory { fun build( url: ArticleModel.Url = ArticleModel.Url("https://example.org/${Random.nextInt()}"), title: String = "Title", - description: String? = null, time: LocalDateTime = LocalDate.today().atTime(0, 0), + description: String? = null, + imageUrl: String? = null, source: ArticleModel.Source = ArticleModel.Source.Blog, ) = ArticleModel( url = url, title = title, description = description, + imageUrl = imageUrl, time = time, source = source, ) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e60ad12e3..ad91acfef 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,6 +12,8 @@ sqldelight = "2.1.0" dataStoreVersion = "1.1.4" junitKtx = "1.3.0" mokoPermissions = "0.20.1" +ktor = "3.3.1" +coil = "3.3.0" [plugins] @@ -49,6 +51,11 @@ window-size = { module = "org.jetbrains.compose.material3:material3-window-size- back-handler = { module = "org.jetbrains.compose.ui:ui-backhandler", version.ref = "compose-plugin" } material-icons = { module = "org.jetbrains.compose.material:material-icons-core", version = "1.7.3" } dark-mode-detector = { module = "io.github.kdroidfilter:platformtools.darkmodedetector", version = "0.7.4" } +coil = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } +coil-network = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" } +coil-network-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" } +coil-network-ios = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } +coil-network-jvm = { module = "io.ktor:ktor-client-java", version.ref = "ktor" } # Preferences androidx-datastore-core-okio = { group = "androidx.datastore", name = "datastore-core-okio", version.ref = "dataStoreVersion" } @@ -134,26 +141,33 @@ tooling = [ "markdown", "kottie", "web-view", + "coil", + "coil-network", ] android = [ -# "android-oonimkall", + # "android-oonimkall", "android-activity", "android-fragment", "android-work", "sqldelight-android", "android-appcompat", + "coil-network-android", ] mobile = [ "moko-permissions-compose", "moko-permissions-notifications", ] +ios = [ + "coil-network-ios", +] desktop = [ "sqldelight-jvm", "androidx-datastore-core-jvm", "directories", "auto-launch", "pratanumandal-unique", - "desktop-oonimkall" + "desktop-oonimkall", + "coil-network-jvm", ] full = [ "sentry",