diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/rss/RssSourceEditDialog.kt b/app/src/main/java/dev/dimension/flare/ui/screen/rss/RssSourceEditDialog.kt index 481eab58..d6f48d9e 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/rss/RssSourceEditDialog.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/rss/RssSourceEditDialog.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size import androidx.compose.foundation.text.input.TextFieldLineLimits +import androidx.compose.foundation.text.input.delete import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.material3.AlertDialog import androidx.compose.material3.CircularProgressIndicator @@ -11,6 +12,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -162,6 +164,22 @@ private fun presenter(id: Int?) = } } } + state.defaultTitle.onSuccess { + DisposableEffect(it) { + if (titleText.text.isEmpty()) { + titleText.edit { + append(it) + } + } + onDispose { + if (titleText.text == it) { + titleText.edit { + delete(0, it.length) + } + } + } + } + } LaunchedEffect(urlText.text) { state.setUrl(urlText.text.toString()) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 189e0135..f2381371 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -127,6 +127,7 @@ ktorfit-converters-flow = { group = "de.jensklingenberg.ktorfit", name = "ktorfi ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktor" } ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" } +ktor-serialization-kotlinx-xml = { group = "io.ktor", name = "ktor-serialization-kotlinx-xml", version.ref = "ktor" } ktor-client-logging = { group = "io.ktor", name = "ktor-client-logging", version.ref = "ktor" } ktor-client-darwin = { group = "io.ktor", name = "ktor-client-darwin", version.ref = "ktor" } ktor-client-okhttp = { group = "io.ktor", name = "ktor-client-okhttp", version.ref = "ktor" } @@ -177,7 +178,7 @@ compose = ["ui", "ui-util", "ui-graphics", "ui-tooling", "ui-tooling-preview", " navigation = ["navigation-compose"] kotlinx = ["kotlinx-datetime", "kotlinx-immutable", "kotlinx-serialization-json", "kotlinx-coroutines-core"] koin = ["koin-core", "koin-android", "koin-compose", "koin-androidx-compose"] -ktor = ["ktor-client-content-negotiation", "ktor-serialization-kotlinx-json", "ktor-client-logging"] +ktor = ["ktor-client-content-negotiation", "ktor-serialization-kotlinx-json", "ktor-client-logging", "ktor-serialization-kotlinx-xml"] coil3 = ["coil3-compose", "coil3-svg", "coil3-ktor3"] coil3-extensions = ["apng", "awebp", "vectordrawable-animated", "zoomable-image", "coil3-gif"] accompanist = ["accompanist-permissions", "accompanist-drawablepainter"] diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/rss/RssService.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/rss/RssService.kt index 1687d6a9..a95019f5 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/rss/RssService.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/rss/RssService.kt @@ -2,8 +2,11 @@ package dev.dimension.flare.data.network.rss import dev.dimension.flare.data.network.ktorClient import dev.dimension.flare.data.network.rss.model.Feed +import io.ktor.client.call.body +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.request.get -import io.ktor.client.statement.bodyAsText +import io.ktor.http.ContentType +import io.ktor.serialization.kotlinx.xml.xml import nl.adaptivity.xmlutil.serialization.XML internal object RssService { @@ -11,12 +14,16 @@ internal object RssService { XML { defaultPolicy { autoPolymorphic = true - ignoreNamespaces() ignoreUnknownChildren() } defaultToGenericParser = true } } - suspend fun fetch(url: String): Feed = xml.decodeFromString(Feed.serializer(), ktorClient().get(url).bodyAsText()) + suspend fun fetch(url: String): Feed = + ktorClient(config = { + install(ContentNegotiation) { + xml(xml, contentType = ContentType.Any) + } + }).get(url).body() } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/rss/model/Feed.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/rss/model/Feed.kt index 6e55eead..62286ee2 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/rss/model/Feed.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/rss/model/Feed.kt @@ -9,7 +9,7 @@ import nl.adaptivity.xmlutil.serialization.XmlValue @Serializable internal sealed interface Feed { @Serializable - @SerialName("feed") + @XmlSerialName("feed") data class Atom( @XmlSerialName("id") @XmlElement(true) @@ -243,14 +243,14 @@ internal sealed interface Feed { } @Serializable - @SerialName("rss") + @XmlSerialName("rss") data class Rss20( val version: String, @XmlElement(true) val channel: Channel, ) : Feed { @Serializable - @SerialName("channel") + @XmlSerialName("channel") data class Channel( @XmlElement(true) val title: String, @@ -371,7 +371,7 @@ internal sealed interface Feed { ) @Serializable - @SerialName("guid") + @XmlSerialName("guid") data class Guid( @XmlElement(false) val isPermaLink: Boolean = false, @@ -387,4 +387,40 @@ internal sealed interface Feed { val title: String? = null, ) } + + @Serializable + @XmlSerialName("RDF", namespace = "http://www.w3.org/1999/02/22-rdf-syntax-ns#", prefix = "rdf") + data class RDF( + @XmlElement(true) + val channel: Channel, + @XmlSerialName("item", namespace = "http://purl.org/rss/1.0/", prefix = "") + val items: List = emptyList(), + ) : Feed { + @Serializable + @XmlSerialName("channel", namespace = "http://purl.org/rss/1.0/", prefix = "") + data class Channel( + @XmlElement(true) + val title: String, + @XmlElement(true) + val link: String, + @XmlElement(true) + val description: String, + @XmlElement(true) + @XmlSerialName(value = "date", namespace = "http://purl.org/dc/elements/1.1/", prefix = "dc") + val date: String? = null, + ) + + @Serializable + data class Item( + @XmlElement(true) + val title: String, + @XmlElement(true) + val link: String, + @XmlElement(true) + val description: String, + @XmlElement(true) + @XmlSerialName(value = "date", namespace = "http://purl.org/dc/elements/1.1/", prefix = "dc") + val date: String? = null, + ) + } } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/Rss.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/Rss.kt index db743802..45fa902d 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/Rss.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/Rss.kt @@ -10,8 +10,17 @@ internal fun Feed.render(): List = when (this) { is Feed.Atom -> renderAtom() is Feed.Rss20 -> renderRss20() + is Feed.RDF -> renderRdf() } +internal val Feed.title: String + get() = + when (this) { + is Feed.Atom -> this.title.value + is Feed.Rss20 -> this.channel.title + is Feed.RDF -> this.channel.title + } + private fun Feed.Atom.renderAtom(): List = this.entries.map { val descHtml = @@ -51,3 +60,23 @@ private fun Feed.Rss20.renderRss20(): List = ), ) } + +private fun Feed.RDF.renderRdf(): List = + this.items.map { + val descHtml = + it.description.let { + Ksoup.parse(it) + } + val img = descHtml.select("img").firstOrNull() + UiTimeline( + topMessage = null, + content = + UiTimeline.ItemContent.Feed( + title = it.title, + description = descHtml.text(), + url = it.link, + image = img?.attr("src"), + createdAt = it.date?.let { input -> runCatching { Instant.parse(input) }.getOrNull() }?.toUi(), + ), + ) + } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/rss/CheckRssSourcePresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/rss/CheckRssSourcePresenter.kt index 016f36db..5cce753c 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/rss/CheckRssSourcePresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/rss/CheckRssSourcePresenter.kt @@ -10,6 +10,8 @@ import dev.dimension.flare.data.network.rss.RssService import dev.dimension.flare.ui.model.UiState import dev.dimension.flare.ui.model.collectAsUiState import dev.dimension.flare.ui.model.flatMap +import dev.dimension.flare.ui.model.map +import dev.dimension.flare.ui.model.mapper.title import dev.dimension.flare.ui.presenter.PresenterBase import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview @@ -20,6 +22,7 @@ import kotlinx.coroutines.flow.flow public class CheckRssSourcePresenter : PresenterBase() { public interface State { public val isValid: UiState + public val defaultTitle: UiState public fun setUrl(value: String) } @@ -28,7 +31,7 @@ public class CheckRssSourcePresenter : PresenterBase