Skip to content

Commit

Permalink
Parse published date in local delegate
Browse files Browse the repository at this point in the history
  • Loading branch information
jocmp committed Dec 25, 2023
1 parent 1c80a80 commit 9b08760
Show file tree
Hide file tree
Showing 13 changed files with 145 additions and 25 deletions.
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ dependencies {
implementation("androidx.activity:activity-compose:1.8.2")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.material:material:1.5.4")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.core:core-ktx:1.12.0")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,13 @@ class AccountViewModel(
onComplete()
}

suspend fun selectArticle(articleID: String) {
suspend fun refreshFeed() {
val id = feedID.value ?: return

account?.refreshFeed(id)
}

fun selectArticle(articleID: String) {
articleState.value = account?.findArticle(articleID.toLong(), feedID.value?.toLongOrNull())
}

Expand Down
52 changes: 39 additions & 13 deletions app/src/main/java/com/jocmp/basilreader/ui/articles/ArticleList.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
package com.jocmp.basilreader.ui.articles

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
Expand All @@ -16,30 +24,48 @@ import androidx.paging.compose.collectAsLazyPagingItems
import com.jocmp.basil.Article
import kotlinx.coroutines.launch

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun ArticleList(
pager: Pager<Int, Article>,
onRefresh: suspend () -> Unit,
onSelect: suspend (articleID: String) -> Unit,
) {
val composableScope = rememberCoroutineScope()
val lazyPagingItems = pager.flow.collectAsLazyPagingItems()

LazyColumn(Modifier.fillMaxWidth()) {
items(count = lazyPagingItems.itemCount) { index ->
val item = lazyPagingItems[index]
Column(
modifier = Modifier
.padding(8.dp)
.clickable {
item?.let {
composableScope.launch {
onSelect(it.id)
val refreshScope = rememberCoroutineScope()
val (refreshing, setRefreshing) = remember { mutableStateOf(false) }

fun refresh() = refreshScope.launch {
setRefreshing(true)
onRefresh()
setRefreshing(false)
}

val state = rememberPullRefreshState(refreshing, ::refresh)

Box(Modifier.pullRefresh(state)) {

LazyColumn(Modifier.fillMaxWidth()) {
items(count = lazyPagingItems.itemCount) { index ->
val item = lazyPagingItems[index]
Column(
modifier = Modifier
.padding(8.dp)
.clickable {
item?.let {
composableScope.launch {
onSelect(it.id)
}
}
}
}
) {
Text(item?.title ?: "No title", fontSize = 20.sp)
) {
Text(item?.title ?: "No title", fontSize = 20.sp)
}
}
}

PullRefreshIndicator(refreshing, state, Modifier.align(Alignment.TopCenter))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ fun ArticleScreen(
viewModel.articles()?.let { pager ->
ArticleList(
pager = pager,
onRefresh = {
viewModel.refreshFeed()
},
onSelect = {
viewModel.selectArticle(it)
navigateToDetail()
Expand Down
27 changes: 23 additions & 4 deletions basil/src/main/java/com/jocmp/basil/Account.kt
Original file line number Diff line number Diff line change
Expand Up @@ -86,17 +86,17 @@ data class Account(
coroutineScope {
launch {
val items = delegate.fetchAll(feed)
val feedID = feed.id.toLong()

items.forEach { item ->
database.articlesQueries.create(
feed_id = feedID,
feed_id = feed.primaryKey,
external_id = item.externalID,
title = item.title,
content_html = item.contentHTML,
url = item.url,
summary = item.summary,
image_url = item.imageURL
image_url = item.imageURL,
published_at = item.publishedAt?.toEpochSecond()
)
}
}
Expand Down Expand Up @@ -124,7 +124,26 @@ data class Account(
return feed
}

suspend fun findArticle(articleID: Long?, feedID: Long?): Article? {
suspend fun refreshFeed(feedID: String) {
val feed = flattenedFeeds.find { it.id == feedID } ?: return

val items = delegate.fetchAll(feed)

items.forEach { item ->
database.articlesQueries.create(
feed_id = feed.primaryKey,
external_id = item.externalID,
title = item.title,
content_html = item.contentHTML,
url = item.url,
summary = item.summary,
image_url = item.imageURL,
published_at = item.publishedAt?.toEpochSecond()
)
}
}

fun findArticle(articleID: Long?, feedID: Long?): Article? {
articleID ?: return null

return database.articlesQueries.findBy(
Expand Down
3 changes: 3 additions & 0 deletions basil/src/main/java/com/jocmp/basil/Feed.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ data class Feed(
val feedURL: String,
val siteURL: String = ""
) {
internal val primaryKey: Long
get() = id.toLong()

override fun equals(other: Any?): Boolean {
if (other is Feed) {
return id == other.id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package com.jocmp.basil.accounts
import com.jocmp.basil.Account
import com.jocmp.basil.Feed
import com.jocmp.basil.feeds.ExternalFeed
import com.jocmp.basil.shared.parseISODate
import com.prof18.rssparser.RssParser
import com.prof18.rssparser.model.RssItem
import java.net.URL
import java.time.ZonedDateTime

internal class LocalAccountDelegate(private val account: Account) : AccountDelegate {
override suspend fun createFeed(feedURL: URL): ExternalFeed {
Expand Down Expand Up @@ -36,7 +38,8 @@ internal class LocalAccountDelegate(private val account: Account) : AccountDeleg
contentHTML = item.content,
url = item.link,
summary = item.description,
imageURL = item.image
imageURL = item.image,
publishedAt = parseISODate(item.pubDate)
)
}
}
Expand Down
3 changes: 3 additions & 0 deletions basil/src/main/java/com/jocmp/basil/accounts/ParsedItem.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package com.jocmp.basil.accounts

import java.time.OffsetDateTime

internal data class ParsedItem(
val externalID: String,
val title: String? = null,
val contentHTML: String? = null,
val url: String? = null,
val summary: String? = null,
val imageURL: String? = null,
val publishedAt: OffsetDateTime?
)
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ internal fun articleMapper(
url: String?,
summary: String?,
imageURL: String?,
datePublished: Long?
publishedAt: Long?
): Article {
return Article(
id = id.toString(),
Expand Down
15 changes: 15 additions & 0 deletions basil/src/main/java/com/jocmp/basil/shared/TimeHelpers.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.jocmp.basil.shared

import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeParseException

fun parseISODate(value: String?): OffsetDateTime? {
value ?: return null

return try {
OffsetDateTime.parse(value).withOffsetSameInstant(ZoneOffset.UTC)
} catch (_: DateTimeParseException) {
null
}
}
9 changes: 5 additions & 4 deletions basil/src/main/sqldelight/com/jocmp/basil/db/articles.sq
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@


CREATE TABLE articles(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
external_id TEXT NOT NULL UNIQUE,
Expand All @@ -9,7 +7,7 @@ CREATE TABLE articles(
url TEXT,
summary TEXT,
image_url TEXT,
date_published INTEGER
published_at INTEGER
);

countByFeed:
Expand All @@ -21,6 +19,7 @@ allByFeed:
SELECT *
FROM articles
WHERE feed_id = :feedID
ORDER BY published_at DESC
LIMIT :limit OFFSET :offset;

findBy:
Expand All @@ -37,7 +36,8 @@ REPLACE INTO articles(
content_html,
url,
summary,
image_url
image_url,
published_at
)
VALUES (
?,
Expand All @@ -46,5 +46,6 @@ VALUES (
?,
?,
?,
?,
?
);
1 change: 0 additions & 1 deletion basil/src/test/java/com/jocmp/basil/AccountTest.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.jocmp.basil

import com.jocmp.basil.accounts.AccountDelegate
import com.jocmp.basil.accounts.LocalAccountDelegate
import com.jocmp.basil.db.Database
import com.jocmp.feedfinder.FeedFinder
Expand Down
41 changes: 41 additions & 0 deletions basil/src/test/java/com/jocmp/basil/shared/TimeHelpersTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.jocmp.basil.shared


import org.junit.Test
import java.time.OffsetDateTime
import java.time.ZoneOffset
import kotlin.test.assertEquals

class TimeHelpersTest {
@Test
fun `parseISODate parses an offset ISO timestamp to UTC`() {
val result = parseISODate("2023-12-25T09:00:00-05:00")

val expected = OffsetDateTime.of(
2023,
12,
25,
14,
0,
0,
0,
ZoneOffset.UTC
)

assertEquals(expected = expected, actual = result)
}

@Test
fun `parseISODate discards non-ISO formatted strings`() {
val result = parseISODate("Mon, 25 Dec 2023 17:18:03 +0000")

assertEquals(expected = null, actual = result)
}

@Test
fun `parseISODate discards null values`() {
val result = parseISODate(null)

assertEquals(expected = null, actual = result)
}
}

0 comments on commit 9b08760

Please sign in to comment.