Display feed image in FeedBottomSheet when available

This commit is contained in:
Shinokuni 2024-11-17 19:50:36 +01:00
parent cbd5c1bc3d
commit db04cdddb7
19 changed files with 133 additions and 45 deletions

View File

@ -28,6 +28,7 @@ class ATOMFeedAdapter : XmlAdapter<Pair<Feed, List<Item>>> {
"title" -> name = nonNullText()
"link" -> parseLink(this@allChildrenAutoIgnore, feed)
"subtitle" -> description = nullableText()
"logo" -> imageUrl = nullableText()
"entry" -> items += itemAdapter.fromXml(this@allChildrenAutoIgnore)
else -> skipContents()
}
@ -52,6 +53,6 @@ class ATOMFeedAdapter : XmlAdapter<Pair<Feed, List<Item>>> {
}
companion object {
val names = Names.of("title", "link", "subtitle", "entry")
val names = Names.of("title", "link", "subtitle", "logo", "entry")
}
}

View File

@ -5,7 +5,9 @@ import com.readrops.api.utils.extensions.nextNonEmptyString
import com.readrops.api.utils.extensions.nextNullableString
import com.readrops.db.entities.Feed
import com.readrops.db.entities.Item
import com.squareup.moshi.*
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
class JSONFeedAdapter : JsonAdapter<Pair<Feed, List<Item>>>() {
@ -27,8 +29,9 @@ class JSONFeedAdapter : JsonAdapter<Pair<Feed, List<Item>>>() {
0 -> name = reader.nextNonEmptyString()
1 -> siteUrl = reader.nextNullableString()
2 -> url = reader.nextNullableString()
3 -> description = reader.nextNullableString()
4 -> items += itemAdapter.fromJson(reader)
3 -> imageUrl = reader.nextNullableString()
4 -> description = reader.nextNullableString()
5 -> items += itemAdapter.fromJson(reader)
else -> reader.skipValue()
}
}
@ -42,6 +45,6 @@ class JSONFeedAdapter : JsonAdapter<Pair<Feed, List<Item>>>() {
companion object {
val names: JsonReader.Options = JsonReader.Options.of("title", "home_page_url",
"feed_url", "description", "items")
"feed_url", "icon", "description", "items")
}
}

View File

@ -40,8 +40,10 @@ class RSS1FeedAdapter : XmlAdapter<Pair<Feed, List<Item>>> {
}
private fun parseChannel(konsumer: Konsumer, feed: Feed) = with(konsumer) {
feed.url = attributes.getValueOrNull("about",
namespace = "http://www.w3.org/1999/02/22-rdf-syntax-ns#")
feed.url = attributes.getValueOrNull(
localName = "about",
namespace = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
)
allChildrenAutoIgnore(names) {
with(feed) {
@ -49,12 +51,16 @@ class RSS1FeedAdapter : XmlAdapter<Pair<Feed, List<Item>>> {
"title" -> name = nonNullText()
"link" -> siteUrl = nonNullText()
"description" -> description = nullableText()
"image" -> imageUrl = attributes.getValueOrNull(
localName = "resource",
namespace = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
)
}
}
}
}
companion object {
val names = Names.of("title", "link", "description")
val names = Names.of("title", "link", "description", "image")
}
}

View File

@ -35,6 +35,7 @@ class RSS2FeedAdapter : XmlAdapter<Pair<Feed, List<Item>>> {
url = attributes.getValueOrNull("href")
}
"item" -> items += itemAdapter.fromXml(this@allChildrenAutoIgnore)
"image" -> imageUrl = parseImage(this@allChildrenAutoIgnore)
else -> skipContents()
}
}
@ -49,7 +50,20 @@ class RSS2FeedAdapter : XmlAdapter<Pair<Feed, List<Item>>> {
}
}
private fun parseImage(konsumer: Konsumer): String? = with(konsumer) {
var url: String? = null
allChildrenAutoIgnore(Names.of("url")) {
when (tagName) {
"url" -> url = nullableText()
else -> skipContents()
}
}
url
}
companion object {
val names = Names.of("title", "description", "link", "item")
val names = Names.of("title", "description", "link", "item", "image")
}
}

View File

@ -27,6 +27,7 @@ class ATOMAdapterTest {
assertEquals(url, "https://github.com/readrops/Readrops/commits/develop.atom")
assertEquals(siteUrl, "https://github.com/readrops/Readrops/commits/develop")
assertEquals(description, "Here is a subtitle")
assertEquals(imageUrl, "https://github.com/readrops/Readrops/blob/develop/images/readrops_logo.png")
}
with(items[0]) {

View File

@ -35,6 +35,7 @@ class JSONFeedAdapterTest {
assertEquals(url, "http://flyingmeat.com/blog/feed.json")
assertEquals(siteUrl, "http://flyingmeat.com/blog/")
assertEquals(description, "News from your friends at Flying Meat.")
assertEquals(imageUrl, "https://secure.flyingmeat.com/favicon.ico")
}
with(items[0]) {

View File

@ -28,6 +28,7 @@ class RSS1AdapterTest {
assertEquals(url, "https://slashdot.org/")
assertEquals(siteUrl, "https://slashdot.org/")
assertEquals(description, "News for nerds, stuff that matters")
assertEquals(imageUrl, "https://a.fsdn.com/sd/topics/topicslashdot.gif")
}
with(items[0]) {

View File

@ -27,6 +27,7 @@ class RSS2AdapterTest {
assertEquals(url, "https://news.ycombinator.com/feed/")
assertEquals(siteUrl, "https://news.ycombinator.com/")
assertEquals(description, "Links for the intellectually curious, ranked by readers.")
assertEquals(imageUrl, "https://news.ycombinator.com/y18.svg")
}
with(items[0]) {

View File

@ -6,6 +6,7 @@
<title>Recent Commits to Readrops:develop</title>
<updated>2020-09-06T21:09:59Z</updated>
<subtitle>Here is a subtitle</subtitle>
<logo>https://github.com/readrops/Readrops/blob/develop/images/readrops_logo.png</logo>
<entry>
<id>tag:github.com,2008:Grit::Commit/c15f093a1bc4211e85f8d1817c9073e307afe5ac</id>
<link type="text/html" rel="alternate" href="https://github.com/readrops/Readrops/commit/c15f093a1bc4211e85f8d1817c9073e307afe5ac"/>

View File

@ -4,6 +4,7 @@
"home_page_url": "http://flyingmeat.com/blog/",
"feed_url": "http://flyingmeat.com/blog/feed.json",
"description": "News from your friends at Flying Meat.",
"icon": "https://secure.flyingmeat.com/favicon.ico",
"author": {
"name": "Gus Mueller"
},

View File

@ -6,6 +6,11 @@
<atom:link href="https://news.ycombinator.com/feed/" rel="self" />
<link>https://news.ycombinator.com/</link>
<description>Links for the intellectually curious, ranked by readers.</description>
<image>
<title>Hacker News</title>
<url>https://news.ycombinator.com/y18.svg</url>
<link>https://news.ycombinator.com/</link>
</image>
<item>
<title>Africa declared free of wild polio</title>
<link>https://www.bbc.com/news/world-africa-53887947</link>

View File

@ -20,11 +20,16 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.blur
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import com.readrops.app.R
import com.readrops.app.util.theme.LargeSpacer
@ -46,53 +51,87 @@ fun FeedModalBottomSheet(
canDeleteFeed: Boolean
) {
ModalBottomSheet(
dragHandle = null,
onDismissRequest = { onDismissRequest() }
) {
Column {
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(
horizontal = MaterialTheme.spacing.largeSpacing
)
Box(
modifier = Modifier.fillMaxWidth()
) {
AsyncImage(
model = feed.iconUrl,
contentDescription = feed.name!!,
placeholder = painterResource(id = R.drawable.ic_rss_feed_grey),
error = painterResource(id = R.drawable.ic_rss_feed_grey),
modifier = Modifier.size(MaterialTheme.spacing.veryLargeSpacing)
)
if (feed.imageUrl != null) {
AsyncImage(
model = feed.imageUrl,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.matchParentSize()
.drawWithContent {
drawContent()
drawRect(
color = Color.Black.copy(alpha = 0.65f)
)
}
.blur(2.5.dp)
)
}
MediumSpacer()
Column {
Text(
text = feed.name!!,
style = MaterialTheme.typography.titleLarge,
maxLines = 1,
overflow = TextOverflow.Ellipsis
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(
top = MaterialTheme.spacing.largeSpacing,
start = MaterialTheme.spacing.largeSpacing,
end = MaterialTheme.spacing.largeSpacing,
bottom = MaterialTheme.spacing.mediumSpacing
)
) {
AsyncImage(
model = feed.iconUrl,
contentDescription = feed.name!!,
placeholder = painterResource(id = R.drawable.ic_rss_feed_grey),
error = painterResource(id = R.drawable.ic_rss_feed_grey),
modifier = Modifier.size(MaterialTheme.spacing.veryLargeSpacing)
)
if (feed.description != null) {
VeryShortSpacer()
MediumSpacer()
Column {
Text(
text = feed.description!!,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 2,
text = feed.name!!,
style = MaterialTheme.typography.titleLarge,
color = if (feed.imageUrl != null) {
Color.White
} else {
MaterialTheme.colorScheme.onBackground
},
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
if (feed.description != null) {
VeryShortSpacer()
Text(
text = feed.description!!,
style = MaterialTheme.typography.bodyMedium,
color = if (feed.imageUrl != null) {
Color.White
} else {
MaterialTheme.colorScheme.onSurfaceVariant
},
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
}
}
}
MediumSpacer()
HorizontalDivider(
modifier = Modifier.padding(horizontal = MaterialTheme.spacing.mediumSpacing)
)
if (feed.imageUrl == null) {
HorizontalDivider(
modifier = Modifier.padding(horizontal = MaterialTheme.spacing.mediumSpacing)
)
}
MediumSpacer()

View File

@ -44,6 +44,7 @@ class GetFoldersWithFeeds(
id = it.feedId,
name = it.feedName,
iconUrl = it.feedIcon,
imageUrl = it.feedImage,
url = it.feedUrl,
siteUrl = it.feedSiteUrl,
description = it.feedDescription,

View File

@ -2,11 +2,11 @@
"formatVersion": 1,
"database": {
"version": 5,
"identityHash": "0bac941f8b1b6003c35a6d0cdc1f2e13",
"identityHash": "63de09bfed367e5705a0889d928f056d",
"entities": [
{
"tableName": "Feed",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `description` TEXT, `url` TEXT, `siteUrl` TEXT, `last_updated` TEXT, `color` INTEGER NOT NULL, `icon_url` TEXT, `etag` TEXT, `last_modified` TEXT, `folder_id` INTEGER, `remote_id` TEXT, `account_id` INTEGER NOT NULL, `notification_enabled` INTEGER NOT NULL DEFAULT 1, FOREIGN KEY(`folder_id`) REFERENCES `Folder`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`account_id`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `description` TEXT, `url` TEXT, `image_url` TEXT, `siteUrl` TEXT, `last_updated` TEXT, `color` INTEGER NOT NULL, `icon_url` TEXT, `etag` TEXT, `last_modified` TEXT, `folder_id` INTEGER, `remote_id` TEXT, `account_id` INTEGER NOT NULL, `notification_enabled` INTEGER NOT NULL DEFAULT 1, FOREIGN KEY(`folder_id`) REFERENCES `Folder`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`account_id`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
@ -32,6 +32,12 @@
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "imageUrl",
"columnName": "image_url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "siteUrl",
"columnName": "siteUrl",
@ -520,7 +526,7 @@
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0bac941f8b1b6003c35a6d0cdc1f2e13')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '63de09bfed367e5705a0889d928f056d')"
]
}
}

View File

@ -121,5 +121,8 @@ object MigrationFrom4To5 : Migration(4, 5) {
db.execSQL("DROP TABLE IF EXISTS `Account`")
db.execSQL("ALTER TABLE `_new_Account` RENAME TO `Account`")
// add image_url field
db.execSQL("""ALTER TABLE `Feed` ADD `image_url` TEXT DEFAULT NULL""")
}
}

View File

@ -28,6 +28,7 @@ data class Feed(
var name: String? = null,
var description: String? = null,
var url: String? = null,
@ColumnInfo("image_url") var imageUrl: String? = null,
var siteUrl: String? = null,
@ColumnInfo("last_updated") var lastUpdated: String? = null,
@ColorInt var color: Int = 0,

View File

@ -16,6 +16,7 @@ data class FolderWithFeed(
val feedId: Int = 0,
val feedName: String? = null,
val feedIcon: String? = null,
val feedImage: String? = null,
val feedUrl: String? = null,
val feedDescription: String? = null,
val feedSiteUrl: String? = null,

View File

@ -11,6 +11,7 @@ object FoldersAndFeedsQueryBuilder {
"Feed.name As feedName",
"Feed.icon_url As feedIcon",
"Feed.url As feedUrl",
"Feed.image_url as feedImage",
"Feed.siteUrl As feedSiteUrl",
"Feed.description as feedDescription",
"Feed.remote_id as feedRemoteId",

View File

@ -49,6 +49,7 @@ lifecyle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-comp
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
coil-http = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" }
coil-svg = { module = "io.coil-kt.coil3:coil-svg", version.ref = "coil" }
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
@ -112,7 +113,7 @@ compose = ["compose-foundation", "compose-runtime", "compose-animation",
voyager = ["voyager-navigator", "voyager-screenmodel", "voyager-tab-navigator", "voyager-koin", "voyager-transitions"]
lifecycle = ["lifecycle-viewmodel-ktx", "lifecycle-viewmodel-compose", "lifecycle-viewmodel-savedstate",
"lifecyle-runtime-compose"]
coil = ["coil-compose", "coil-http"]
coil = ["coil-compose", "coil-http", "coil-svg"]
coroutines = ["coroutines-core", "coroutines-android"]
room = ["room-runtime", "room-ktx", "room-paging"]
koin = ["koin-core", "koin-android", "koin-androidx-compose"]