diff --git a/app/src/main/java/com/jocmp/basilreader/ui/accounts/AccountViewModel.kt b/app/src/main/java/com/jocmp/basilreader/ui/accounts/AccountViewModel.kt index f8ee75ba..b27b9cd8 100644 --- a/app/src/main/java/com/jocmp/basilreader/ui/accounts/AccountViewModel.kt +++ b/app/src/main/java/com/jocmp/basilreader/ui/accounts/AccountViewModel.kt @@ -89,7 +89,7 @@ class AccountViewModel( fun selectArticle(articleID: String) { account.markRead(articleID) - articleState.value = account.findArticle(articleID.toLong()) + articleState.value = account.findArticle(articleID = articleID) } fun toggleArticleRead() { @@ -104,6 +104,18 @@ class AccountViewModel( } } + fun toggleArticleStar() { + articleState.value?.let { article -> + if (article.starred) { + account.removeStar(article.id) + } else { + account.addStar(article.id) + } + + articleState.value = article.copy(starred = !article.starred) + } + } + fun clearArticle() { articleState.value = null } diff --git a/app/src/main/java/com/jocmp/basilreader/ui/articles/ArticleFilterNavigationBar.kt b/app/src/main/java/com/jocmp/basilreader/ui/articles/ArticleFilterNavigationBar.kt index c70ea6ab..4176b517 100644 --- a/app/src/main/java/com/jocmp/basilreader/ui/articles/ArticleFilterNavigationBar.kt +++ b/app/src/main/java/com/jocmp/basilreader/ui/articles/ArticleFilterNavigationBar.kt @@ -26,7 +26,12 @@ fun ArticleFilterNavigationBar( NavigationBar { NavigationBarItem( - icon = { Icon(Icons.Filled.Favorite, contentDescription = null) }, + icon = { + Icon( + painterResource(R.drawable.icon_star_filled), + contentDescription = null + ) + }, label = { Text(stringResource(id = R.string.article_filters_starred)) }, selected = selected === ArticleStatus.STARRED, onClick = { checkedSelect(ArticleStatus.STARRED) }, @@ -35,7 +40,7 @@ fun ArticleFilterNavigationBar( NavigationBarItem( icon = { Icon( - painter = painterResource(R.drawable.unread), + painterResource(R.drawable.icon_circle_filled), contentDescription = null ) }, @@ -47,7 +52,7 @@ fun ArticleFilterNavigationBar( NavigationBarItem( icon = { Icon( - painter = painterResource(R.drawable.notes), + painter = painterResource(R.drawable.icon_notes), contentDescription = null ) }, diff --git a/app/src/main/java/com/jocmp/basilreader/ui/articles/ArticleScreen.kt b/app/src/main/java/com/jocmp/basilreader/ui/articles/ArticleScreen.kt index 0cb9bb2e..a81da4d7 100644 --- a/app/src/main/java/com/jocmp/basilreader/ui/articles/ArticleScreen.kt +++ b/app/src/main/java/com/jocmp/basilreader/ui/articles/ArticleScreen.kt @@ -113,6 +113,7 @@ fun ArticleScreen( ArticleView( article = viewModel.article, onToggleRead = viewModel::toggleArticleRead, + onToggleStar = viewModel::toggleArticleStar, onBackPressed = { viewModel.clearArticle() setDestination(ListDetailPaneScaffoldRole.List) diff --git a/app/src/main/java/com/jocmp/basilreader/ui/articles/ArticleView.kt b/app/src/main/java/com/jocmp/basilreader/ui/articles/ArticleView.kt index 9d9de2b3..056c00bd 100644 --- a/app/src/main/java/com/jocmp/basilreader/ui/articles/ArticleView.kt +++ b/app/src/main/java/com/jocmp/basilreader/ui/articles/ArticleView.kt @@ -2,18 +2,22 @@ package com.jocmp.basilreader.ui.articles import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CheckCircle -import androidx.compose.material.icons.filled.Phone +import androidx.compose.material.icons.filled.Star import androidx.compose.material.icons.outlined.CheckCircle -import androidx.compose.material.icons.outlined.Phone +import androidx.compose.material.icons.outlined.Star import androidx.compose.material3.Button import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource import com.jocmp.basil.Article +import com.jocmp.basilreader.R import com.jocmp.basilreader.ui.components.EmptyView import com.jocmp.basilreader.ui.components.WebView import com.jocmp.basilreader.ui.components.rememberWebViewStateWithHTMLData @@ -23,11 +27,13 @@ fun ArticleView( article: Article?, onBackPressed: () -> Unit, onToggleRead: () -> Unit, + onToggleStar: () -> Unit ) { if (article != null) { ArticleLoadedView( article = article, - onToggleRead = onToggleRead + onToggleRead = onToggleRead, + onToggleStar = onToggleStar ) } else { EmptyView() @@ -42,19 +48,31 @@ fun ArticleView( fun ArticleLoadedView( article: Article, onToggleRead: () -> Unit, + onToggleStar: () -> Unit ) { val state = rememberWebViewStateWithHTMLData(article.contentHTML) - val image = if (article.read) { - Icons.Outlined.CheckCircle + val readIcon = if (article.read) { + R.drawable.icon_circle_outline } else { - Icons.Filled.CheckCircle + R.drawable.icon_circle_filled + } + + val starIcon = if (article.starred) { + R.drawable.icon_star_filled + } else { + R.drawable.icon_star_outline } Scaffold( topBar = { - Button(onClick = { onToggleRead() }) { - Icon(imageVector = image, contentDescription = null) + Row { + IconButton(onClick = { onToggleRead() }) { + Icon(painterResource(id = readIcon), contentDescription = null) + } + IconButton(onClick = { onToggleStar() }) { + Icon(painterResource(id = starIcon), contentDescription = null) + } } } ) { innerPadding -> diff --git a/app/src/main/res/drawable/unread.xml b/app/src/main/res/drawable/icon_circle_filled.xml similarity index 89% rename from app/src/main/res/drawable/unread.xml rename to app/src/main/res/drawable/icon_circle_filled.xml index e7b84944..92d73bf8 100644 --- a/app/src/main/res/drawable/unread.xml +++ b/app/src/main/res/drawable/icon_circle_filled.xml @@ -1,6 +1,6 @@ + + diff --git a/app/src/main/res/drawable/notes.xml b/app/src/main/res/drawable/icon_notes.xml similarity index 100% rename from app/src/main/res/drawable/notes.xml rename to app/src/main/res/drawable/icon_notes.xml diff --git a/app/src/main/res/drawable/icon_star_filled.xml b/app/src/main/res/drawable/icon_star_filled.xml new file mode 100644 index 00000000..1205fcaf --- /dev/null +++ b/app/src/main/res/drawable/icon_star_filled.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/icon_star_outline.xml b/app/src/main/res/drawable/icon_star_outline.xml new file mode 100644 index 00000000..2ec3545f --- /dev/null +++ b/app/src/main/res/drawable/icon_star_outline.xml @@ -0,0 +1,9 @@ + + + diff --git a/basil/src/main/java/com/jocmp/basil/Account.kt b/basil/src/main/java/com/jocmp/basil/Account.kt index 465c2d5e..8d7b9874 100644 --- a/basil/src/main/java/com/jocmp/basil/Account.kt +++ b/basil/src/main/java/com/jocmp/basil/Account.kt @@ -142,13 +142,16 @@ data class Account( return folders.find { it.title == title } } - fun findArticle(articleID: Long?): Article? { - articleID ?: return null + fun findArticle(articleID: String): Article? { + return articles.fetch(articleID = articleID) + } + + fun addStar(articleID: String) { + articles.addStar(articleID = articleID) + } - return database.articlesQueries.findBy( - articleID = articleID, - mapper = ::articleMapper - ).executeAsOneOrNull() + fun removeStar(articleID: String) { + articles.removeStar(articleID = articleID) } fun markRead(articleID: String) { @@ -156,7 +159,7 @@ data class Account( } fun markUnread(articleID: String) { - articles.markUnread(articleID) + articles.markUnread(articleID = articleID) } private fun updateArticles(feed: Feed, items: List) { diff --git a/basil/src/main/java/com/jocmp/basil/persistence/ArticleRecords.kt b/basil/src/main/java/com/jocmp/basil/persistence/ArticleRecords.kt index 41d76e5f..230b923f 100644 --- a/basil/src/main/java/com/jocmp/basil/persistence/ArticleRecords.kt +++ b/basil/src/main/java/com/jocmp/basil/persistence/ArticleRecords.kt @@ -12,6 +12,17 @@ class ArticleRecords internal constructor( val byStatus = ByStatus(database) val byFeed = ByFeed(database) + fun fetch(articleID: String): Article? { + val id = articleID.toLongOrNull() + + id ?: return null + + return database.articlesQueries.findBy( + articleID = id, + mapper = ::articleMapper + ).executeAsOneOrNull() + } + fun markRead(articleID: String, lastReadAt: ZonedDateTime = ZonedDateTime.now()) { database.articlesQueries.markRead( articleID = articleID.toLong(), @@ -28,6 +39,20 @@ class ArticleRecords internal constructor( ) } + fun addStar(articleID: String) { + database.articlesQueries.markStarred( + articleID = articleID.toLong(), + starred = true + ) + } + + fun removeStar(articleID: String) { + database.articlesQueries.markStarred( + articleID = articleID.toLong(), + starred = false + ) + } + class ByFeed(private val database: Database) { fun all( feedIDs: List, diff --git a/basil/src/main/sqldelight/com/jocmp/basil/db/articles.sq b/basil/src/main/sqldelight/com/jocmp/basil/db/articles.sq index ef14e2c8..568af063 100644 --- a/basil/src/main/sqldelight/com/jocmp/basil/db/articles.sq +++ b/basil/src/main/sqldelight/com/jocmp/basil/db/articles.sq @@ -105,3 +105,13 @@ WHERE EXISTS ( AND articles.external_id = article_statuses.external_id AND article_statuses.feed_id = articles.feed_id LIMIT 1 ); + +markStarred: +UPDATE article_statuses SET starred = :starred +WHERE EXISTS ( + SELECT id + FROM articles + WHERE articles.id = :articleID + AND articles.external_id = article_statuses.external_id AND article_statuses.feed_id = articles.feed_id + LIMIT 1 +); diff --git a/basil/src/test/java/com/jocmp/basil/persistence/ArticleRecordsTest.kt b/basil/src/test/java/com/jocmp/basil/persistence/ArticleRecordsTest.kt index f79dc591..a3c9fd95 100644 --- a/basil/src/test/java/com/jocmp/basil/persistence/ArticleRecordsTest.kt +++ b/basil/src/test/java/com/jocmp/basil/persistence/ArticleRecordsTest.kt @@ -7,8 +7,8 @@ import com.jocmp.basil.fixtures.ArticleFixture import com.jocmp.basil.repeated import org.junit.Before import org.junit.Test -import kotlin.math.exp import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertTrue class ArticleRecordsTest { @@ -75,4 +75,40 @@ class ArticleRecordsTest { assertEquals(expected = 2, actual = count) assertEquals(actual = actual, expected = expected) } + + @Test + fun markUnread() { + val article = articleFixture.create() + val articleRecords = ArticleRecords(database) + + articleRecords.markUnread(articleID = article.id) + + val reloaded = articleRecords.fetch(articleID = article.id)!! + + assertFalse(reloaded.read) + } + + @Test + fun addStar() { + val article = articleFixture.create() + val articleRecords = ArticleRecords(database) + + articleRecords.addStar(articleID = article.id) + + val reloaded = articleRecords.fetch(articleID = article.id)!! + + assertTrue(reloaded.starred) + } + + @Test + fun removeStar() { + val article = articleFixture.create() + val articleRecords = ArticleRecords(database) + + articleRecords.removeStar(articleID = article.id) + + val reloaded = articleRecords.fetch(articleID = article.id)!! + + assertFalse(reloaded.starred) + } }