diff --git a/app/src/main/java/com/jocmp/capyreader/ui/articles/detail/ArticleTopBar.kt b/app/src/main/java/com/jocmp/capyreader/ui/articles/detail/ArticleTopBar.kt index cfa893ed..c2d3cbc6 100644 --- a/app/src/main/java/com/jocmp/capyreader/ui/articles/detail/ArticleTopBar.kt +++ b/app/src/main/java/com/jocmp/capyreader/ui/articles/detail/ArticleTopBar.kt @@ -20,6 +20,7 @@ import com.jocmp.capyreader.R import com.jocmp.capyreader.common.shareArticle import com.jocmp.capyreader.ui.LocalWindowWidth import com.jocmp.capyreader.ui.fixtures.ArticleSample +import java.net.URL @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -50,12 +51,15 @@ fun ArticleTopBar( ) } - IconButton(onClick = { onToggleExtractContent() }) { - Icon( - painterResource(id = extractIcon(extractedContent)), - contentDescription = stringResource(R.string.extract_full_content) - ) + if (article.extractedContentURL != null) { + IconButton(onClick = { onToggleExtractContent() }) { + Icon( + painterResource(id = extractIcon(extractedContent)), + contentDescription = stringResource(R.string.extract_full_content) + ) + } } + IconButton(onClick = { onToggleStar() }) { Icon( painterResource(id = starredIcon(article)), @@ -121,7 +125,7 @@ fun ArticleNavigationIcon(onClick: () -> Unit) { @Composable private fun ArticleTopBarPreview(@PreviewParameter(ArticleSample::class) article: Article) { ArticleTopBar( - article = article, + article = article.copy(extractedContentURL = URL("https://example.com")), extractedContent = ExtractedContent(), onToggleExtractContent = {}, onToggleRead = {}, diff --git a/app/src/main/java/com/jocmp/capyreader/ui/articles/detail/ExtractedContent.kt b/app/src/main/java/com/jocmp/capyreader/ui/articles/detail/ExtractedContent.kt index df2557aa..7005425c 100644 --- a/app/src/main/java/com/jocmp/capyreader/ui/articles/detail/ExtractedContent.kt +++ b/app/src/main/java/com/jocmp/capyreader/ui/articles/detail/ExtractedContent.kt @@ -13,8 +13,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.koin.compose.koinInject -private const val TAG = "ExtractedContent" - data class ExtractedContent( val requestShow: Boolean = false, val value: Async = Async.Uninitialized, diff --git a/app/src/main/java/com/jocmp/capyreader/ui/articles/list/ArticleActionBottomSheet.kt b/app/src/main/java/com/jocmp/capyreader/ui/articles/list/ArticleActionBottomSheet.kt index 898ac164..e2c2e958 100644 --- a/app/src/main/java/com/jocmp/capyreader/ui/articles/list/ArticleActionBottomSheet.kt +++ b/app/src/main/java/com/jocmp/capyreader/ui/articles/list/ArticleActionBottomSheet.kt @@ -4,16 +4,15 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.ListItem import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.ModalBottomSheetProperties import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -30,7 +29,10 @@ fun ArticleActionBottomSheet( onMarkAllRead: (range: MarkRead) -> Unit = {}, onDismissRequest: () -> Unit = {}, ) { - ModalBottomSheet(onDismissRequest = onDismissRequest) { + ModalBottomSheet( + onDismissRequest = onDismissRequest, + scrimColor = Color.Unspecified, + ) { Column(Modifier.padding(bottom = 32.dp)) { ListItem( leadingContent = { diff --git a/capy/src/main/java/com/jocmp/capy/AccountManager.kt b/capy/src/main/java/com/jocmp/capy/AccountManager.kt index fd65c5ca..6b7b73c0 100644 --- a/capy/src/main/java/com/jocmp/capy/AccountManager.kt +++ b/capy/src/main/java/com/jocmp/capy/AccountManager.kt @@ -29,7 +29,6 @@ class AccountManager( preferences.password.set(password) } - return accountID } @@ -42,6 +41,8 @@ class AccountManager( } } + preferenceStoreProvider.build(accountID).source.set(source) + accountFile(accountID).apply { mkdir() } return accountID @@ -73,6 +74,7 @@ class AccountManager( id = id, path = pathURI, database = database, + source = preferences.source.get(), preferences = preferences, ) } diff --git a/capy/src/test/java/com/jocmp/capy/AccountManagerTest.kt b/capy/src/test/java/com/jocmp/capy/AccountManagerTest.kt index 5ad6afa5..11a15ad6 100644 --- a/capy/src/test/java/com/jocmp/capy/AccountManagerTest.kt +++ b/capy/src/test/java/com/jocmp/capy/AccountManagerTest.kt @@ -1,5 +1,6 @@ package com.jocmp.capy +import com.jocmp.capy.accounts.Source import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.Test @@ -25,14 +26,14 @@ class AccountManagerTest { fun addAccount() { val manager = buildManager() - assertNotNull(manager.createAccount("foo", "bar")) + assertNotNull(manager.createAccount("foo", "bar", Source.LOCAL)) } @Test fun findById() = runBlocking { val manager = buildManager() - val accountID = manager.createAccount("foo", "bar") + val accountID = manager.createAccount("foo", "bar", Source.LOCAL) val account = manager.findByID(accountID) diff --git a/capy/src/test/java/com/jocmp/capy/accounts/LocalAccountDelegateTest.kt b/capy/src/test/java/com/jocmp/capy/accounts/LocalAccountDelegateTest.kt new file mode 100644 index 00000000..b388ed46 --- /dev/null +++ b/capy/src/test/java/com/jocmp/capy/accounts/LocalAccountDelegateTest.kt @@ -0,0 +1,213 @@ +package com.jocmp.capy.accounts + +import com.jocmp.capy.InMemoryDatabaseProvider +import com.jocmp.capy.db.Database +import com.jocmp.capy.fixtures.FeedFixture +import com.jocmp.feedbinclient.Tagging +import com.jocmp.feedfinder.FeedFinder +import com.jocmp.feedfinder.parser.Feed +import com.prof18.rssparser.model.RssChannel +import com.prof18.rssparser.model.RssItem +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import java.net.URL +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class LocalAccountDelegateTest { + private val accountID = "777" + private lateinit var database: Database + private lateinit var feedFinder: FeedFinder + private lateinit var feedFixture: FeedFixture + + private val channel = RssChannel( + title = "Ed Zitron", + link = "http://wheresyoured.at/feed", + items = listOf( + RssItem( + guid = null, + title = "Let Tim Cook", + author = "Ed Zitron", + link = "https://www.wheresyoured.at/untitled/", + pubDate = "Mon, 17 Jun 2024 16:41:38 GMT", + description = "Last week, Apple announced “Apple Intelligence,” a suite of features coming to iOS 18 (the next version of the iPhone", + content = "Last week, Apple announced “Apple Intelligence,” a suite of features coming to iOS 18 (the next version of the iPhone", + image = null, + audio = null, + video = null, + sourceName = null, + sourceUrl = null, + categories = emptyList(), + itunesItemData = null, + commentsUrl = null, + ) + ), + description = null, + image = null, + itunesChannelData = null, + lastBuildDate = null, + updatePeriod = null + ) + + private val taggings = listOf( + Tagging( + id = 1, + feed_id = 2, + name = "Gadgets" + ) + ) + + + @Before + fun setup() { + database = InMemoryDatabaseProvider.build(accountID) + feedFixture = FeedFixture(database) + feedFinder = mockk() + } + + @Test + fun refreshAll_updatesEntries() = runTest { + coEvery { feedFinder.fetch(url = any()) }.returns(Result.success(channel)) + + val delegate = LocalAccountDelegate(database, feedFinder) + + FeedFixture(database).create(feedID = channel.link!!) + + delegate.refresh() + + val articles = database + .articlesQueries + .countAll(read = false, starred = false) + .executeAsList() + + val feeds = database + .feedsQueries + .all() + .executeAsList() + + assertEquals(expected = 1, actual = feeds.size) + assertEquals(expected = 1, actual = articles.size) + } + + @Test + fun markRead() = runTest { + val delegate = LocalAccountDelegate(database, feedFinder) + + assertTrue(delegate.markRead(listOf("777")).isSuccess) + } + + @Test + fun markUnread() = runTest { + val delegate = LocalAccountDelegate(database, feedFinder) + + assertTrue(delegate.markUnread(listOf("777")).isSuccess) + } + + @Test + fun addStar() = runTest { + val delegate = LocalAccountDelegate(database, feedFinder) + + assertTrue(delegate.addStar(listOf("777")).isSuccess) + } + + @Test + fun removeStar() = runTest { + val delegate = LocalAccountDelegate(database, feedFinder) + + assertTrue(delegate.removeStar(listOf("777")).isSuccess) + } + + @Test + fun addFeed() = runTest { + val delegate = LocalAccountDelegate(database, feedFinder) + val url = "wheresyoured.at" + + coEvery { feedFinder.find(url) }.returns( + Result.success( + listOf( + TestFeed( + name = "Ed Zitron", + feedURL = URL(channel.link!!), + siteURL = null, + ) + ) + ) + ) + + val result = delegate.addFeed(url = url) as AddFeedResult.Success + val feed = result.feed + + assertEquals( + expected = "Ed Zitron", + actual = feed.title + ) + } + + @Test + fun addFeed_multipleChoice() = runTest { + val delegate = LocalAccountDelegate(database, feedFinder) + val url = "9to5google.com" + + val choices = listOf( + TestFeed( + name = "9to5Google", + feedURL = URL("https://9to5google.com/feed") + ), + TestFeed( + name = "Comments for 9to5Google", + feedURL = URL("https://9to5google.com/comments/feed") + ), + TestFeed( + name = "Stories Archive - 9to5Google", + feedURL = URL("https://9to5google.com/web-stories/feed"), + ) + ) + + coEvery { feedFinder.find(url) }.returns(Result.success(choices)) + + val result = delegate.addFeed(url = url) + + val actualTitles = (result as AddFeedResult.MultipleChoices).choices.map { it.title } + + assertEquals(expected = choices.map { it.name }, actual = actualTitles) + } + + @Test + fun addFeed_Failure() = runTest { + val delegate = LocalAccountDelegate(database, feedFinder) + val url = "example.com" + + coEvery { feedFinder.find(url) }.returns(Result.failure(Error("Sorry charlie"))) + + val result = delegate.addFeed(url = url) + + assertTrue(result is AddFeedResult.Failure) + } + + @Test + fun updateFeed_modifyTitle() = runTest { + val delegate = LocalAccountDelegate(database, feedFinder) + val feed = feedFixture.create() + + val feedTitle = "The Verge Mobile Podcast" + + val updated = delegate.updateFeed( + feed = feed, + title = feedTitle, + folderTitles = emptyList() + ).getOrThrow() + + assertEquals(expected = feedTitle, actual = updated.title) + } + + private data class TestFeed( + override val name: String, + override val feedURL: URL, + override val siteURL: URL? = null + ) : Feed { + override fun isValid() = true + } +} diff --git a/capy/src/test/java/com/jocmp/capy/fixtures/ArticleFixture.kt b/capy/src/test/java/com/jocmp/capy/fixtures/ArticleFixture.kt index 69f75333..2e60d6ec 100644 --- a/capy/src/test/java/com/jocmp/capy/fixtures/ArticleFixture.kt +++ b/capy/src/test/java/com/jocmp/capy/fixtures/ArticleFixture.kt @@ -31,7 +31,8 @@ class ArticleFixture(private val database: Database) { ) database.articlesQueries.updateStatus( article_id = id, - updated_at = publishedAt + updated_at = publishedAt, + read = true ) } diff --git a/capy/src/test/java/com/jocmp/capy/fixtures/FeedFixture.kt b/capy/src/test/java/com/jocmp/capy/fixtures/FeedFixture.kt index f7c2ae59..865d251d 100644 --- a/capy/src/test/java/com/jocmp/capy/fixtures/FeedFixture.kt +++ b/capy/src/test/java/com/jocmp/capy/fixtures/FeedFixture.kt @@ -6,7 +6,7 @@ import com.jocmp.capy.persistence.FeedRecords import kotlinx.coroutines.runBlocking import java.security.SecureRandom -class FeedFixture(private val database: Database) { +class FeedFixture(database: Database) { private val records = FeedRecords(database) fun create(