Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add article status table #26

Merged
merged 2 commits into from
Dec 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -13,8 +13,9 @@ import com.jocmp.basil.AccountManager
import com.jocmp.basil.Article
import com.jocmp.basil.Feed
import com.jocmp.basil.FeedFormEntry
import com.jocmp.basil.Filter
import com.jocmp.basil.Folder
import com.jocmp.basil.feedPagingSource
import com.jocmp.basil.buildPager
import com.jocmp.basilreader.selectAccount
import com.jocmp.basilreader.selectedAccount
import kotlinx.coroutines.flow.first
Expand All @@ -29,18 +30,22 @@ class AccountViewModel(
policy = neverEqualPolicy()
)

private val pager: MutableState<Pager<Int, Article>?> = mutableStateOf(null)

init {
viewModelScope.launch {
val accountID = settings.data.first().selectedAccount()

if (accountID != null) {
updateState(accountID)
selectAccount(accountID)
} else {
val firstID = accountManager.accountIDs().firstOrNull()
firstID?.let {
selectAccount(firstID)
selectSettingsAccount(firstID)
}
}

pager.value = account?.buildPager()
}
}

Expand All @@ -54,8 +59,6 @@ class AccountViewModel(

private val articleState = mutableStateOf<Article?>(null)

private val pager = mutableStateOf<Pager<Int, Article>?>(null)

val feeds: List<Feed>
get() = account?.feeds?.toList() ?: emptyList()

Expand All @@ -65,29 +68,42 @@ class AccountViewModel(
fun articles(): Pager<Int, Article>? = pager.value

fun selectFeed(feedID: String, onComplete: () -> Unit) {
this.feedID.value = feedID
val feed = account?.findFeed(feedID)

this.feedID.value = feed?.id

if (feed != null) {
pager.value = account?.buildPager(
filter = Filter.Feeds(feed = feed, status = Filter.Status.ALL),
)
}

pager.value = account?.feedPagingSource(this.feedID.value)
clearArticle()

onComplete()
}

suspend fun selectArticle(articleID: String) {
articleState.value = account?.findArticle(articleID.toLong(), feedID.value?.toLongOrNull())
suspend fun refreshFeed() {
val id = feedID.value ?: return

account?.refreshFeed(id)
}

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

fun clearArticle() {
articleState.value = null
}

private fun selectAccount(accountID: String) {
private fun selectSettingsAccount(accountID: String) {
viewModelScope.launch {
settings.selectAccount(accountID)
}
}

private fun updateState(accountID: String) {
private fun selectAccount(accountID: String) {
accountState.value = accountManager.findByID(accountID)
}

Expand Down
54 changes: 42 additions & 12 deletions app/src/main/java/com/jocmp/basilreader/ui/articles/ArticleList.kt
Original file line number Diff line number Diff line change
@@ -1,45 +1,75 @@
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
import androidx.paging.Pager
import androidx.paging.compose.collectAsLazyPagingItems
import com.jocmp.basil.Article
import kotlinx.coroutines.launch
import java.time.format.DateTimeFormatter

@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)
}
}
}
) {
item?.let { article ->
Text(article.title, fontSize = 20.sp)
Text(article.arrivedAt.format(DateTimeFormatter.BASIC_ISO_DATE))
}
) {
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
56 changes: 38 additions & 18 deletions basil/src/main/java/com/jocmp/basil/Account.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ package com.jocmp.basil

import com.jocmp.basil.accounts.AccountDelegate
import com.jocmp.basil.accounts.LocalAccountDelegate
import com.jocmp.basil.articles.articleMapper
import com.jocmp.basil.db.Articles
import com.jocmp.basil.accounts.ParsedItem
import com.jocmp.basil.accounts.asOPML
import com.jocmp.basil.db.Database
import com.jocmp.basil.feeds.FeedRecords
import com.jocmp.basil.opml.Outline
import com.jocmp.basil.opml.asFeed
import com.jocmp.basil.opml.asFolder
import com.jocmp.basil.persistence.FeedRecords
import com.jocmp.basil.persistence.articleMapper
import com.jocmp.basil.shared.nowUTC
import com.jocmp.feedfinder.FeedFinder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
Expand Down Expand Up @@ -52,6 +54,10 @@ data class Account(
addAll(folders.flatMap { it.feeds })
}

fun findFeed(feedID: String): Feed? {
return flattenedFeeds.find { it.id == feedID }
}

suspend fun addFolder(title: String): Folder {
val folder = Folder(title = title)

Expand Down Expand Up @@ -86,19 +92,8 @@ data class Account(
coroutineScope {
launch {
val items = delegate.fetchAll(feed)
val feedID = feed.id.toLong()

items.forEach { item ->
database.articlesQueries.create(
feed_id = feedID,
external_id = item.externalID,
title = item.title,
content_html = item.contentHTML,
url = item.url,
summary = item.summary,
image_url = item.imageURL
)
}

updateArticles(feed, items)
}
}

Expand All @@ -124,16 +119,41 @@ 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)

updateArticles(feed, items)
}

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

return database.articlesQueries.findBy(
articleID = articleID,
feedID = feedID,
mapper = ::articleMapper
).executeAsOneOrNull()
}

private fun updateArticles(feed: Feed, items: List<ParsedItem>) {
items.forEach { item ->
val publishedAt = item.publishedAt?.toEpochSecond()

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 = publishedAt,
arrived_at = publishedAt ?: nowUTC()
)
}
}

private fun entrySiteURL(url: URL?): String {
return url?.toString() ?: ""
}
Expand Down
19 changes: 19 additions & 0 deletions basil/src/main/java/com/jocmp/basil/AccountPager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.jocmp.basil

import androidx.paging.Pager
import androidx.paging.PagingConfig
import com.jocmp.basil.persistence.ArticlePagerFactory

fun Account.buildPager(
filter: Filter = Filter.Articles(status = Filter.Status.ALL)
): Pager<Int, Article> {
val pagerFactory = ArticlePagerFactory(
database = database,
filter = filter
)

return Pager(
config = PagingConfig(pageSize = 10),
pagingSourceFactory = { pagerFactory.find() }
)
}
4 changes: 3 additions & 1 deletion basil/src/main/java/com/jocmp/basil/Article.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.jocmp.basil

import java.net.URL
import java.time.ZonedDateTime

data class Article(
val id: String,
Expand All @@ -10,5 +11,6 @@ data class Article(
val contentHTML: String,
val url: URL?,
val summary: String,
val imageURL: URL?
val imageURL: URL?,
val arrivedAt: ZonedDateTime
)
28 changes: 0 additions & 28 deletions basil/src/main/java/com/jocmp/basil/ArticlesPagerExt.kt

This file was deleted.

9 changes: 3 additions & 6 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 All @@ -20,9 +23,3 @@ data class Feed(
return id.hashCode()
}
}

fun Feed.asOPML(indentLevel: Int): String {
val opml =
"<outline text=\"${name}\" title=\"${name}\" description=\"\" type=\"rss\" version=\"RSS\" htmlUrl=\"${siteURL}\" xmlUrl=\"${feedURL}\" basil_id=\"${id}\"/>\n"
return opml.prepending(tabCount = indentLevel)
}
15 changes: 15 additions & 0 deletions basil/src/main/java/com/jocmp/basil/Filter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.jocmp.basil

sealed class Filter(val status: Status) {
enum class Status(value: String) {
ALL("all"),
READ("read"),
STARRED("starred")
}

class Articles(status: Status) : Filter(status)

class Feeds(val feed: Feed, status: Status) : Filter(status)

class Folders(val folder: Folder, status: Status) : Filter(status)
}
Loading