diff --git a/.claude/commands/bench-profile.md b/.claude/skills/bench-profile.md similarity index 100% rename from .claude/commands/bench-profile.md rename to .claude/skills/bench-profile.md diff --git a/.claude/commands/fix-weblate.md b/.claude/skills/fix-weblate.md similarity index 100% rename from .claude/commands/fix-weblate.md rename to .claude/skills/fix-weblate.md diff --git a/.claude/commands/gh-issue.md b/.claude/skills/gh-issue.md similarity index 100% rename from .claude/commands/gh-issue.md rename to .claude/skills/gh-issue.md diff --git a/.claude/commands/profile-refresh.md b/.claude/skills/profile-refresh.md similarity index 100% rename from .claude/commands/profile-refresh.md rename to .claude/skills/profile-refresh.md diff --git a/.claude/commands/profile-select.md b/.claude/skills/profile-select.md similarity index 100% rename from .claude/commands/profile-select.md rename to .claude/skills/profile-select.md diff --git a/.gitignore b/.gitignore index 15a434e95..1a0ddbfe6 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ app/gplay/release/ # Cruft technotes/.obsidian/ +**/.idea/ .idea/runConfigurations.xml .idea/markdown.xml .kotlin/sessions/ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 554e3be57..59cde4e6d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,4 +1,5 @@ import com.android.build.gradle.internal.tasks.factory.dependsOn +import java.security.SecureRandom import java.util.Properties plugins { @@ -193,4 +194,42 @@ tasks.register("useGMSDebugFile") { } } +val generateSecretKey = tasks.register("generateSecretKey") { + val outputDir = layout.buildDirectory.dir("generated/source/secrets/com/capyreader/app") + outputs.dir(outputDir) + + doLast { + val username = secrets.getProperty("extract_username", "") + val secret = secrets.getProperty("extract_secret", "") + val salt = ByteArray(64).also { SecureRandom().nextBytes(it) } + + fun encode(value: String) = value.toByteArray() + .mapIndexed { i, b -> (b.toInt() xor salt[i % salt.size].toInt()).toByte() } + .toByteArray() + + fun ByteArray.toHexLiteral() = joinToString { "0x%02x.toByte()".format(it) } + + val file = outputDir.get().file("SecretKey.kt").asFile + file.parentFile.mkdirs() + file.writeText( + """ + package com.capyreader.app + + internal object SecretKey { + private val salt = byteArrayOf(${salt.toHexLiteral()}) + + private fun decode(encoded: ByteArray) = + String(ByteArray(encoded.size) { i -> (encoded[i].toInt() xor salt[i % salt.size].toInt()).toByte() }) + + val extractUsername = decode(byteArrayOf(${encode(username).toHexLiteral()})) + val extractSecret = decode(byteArrayOf(${encode(secret).toHexLiteral()})) + } + """.trimIndent() + ) + } +} + project.tasks.preBuild.dependsOn("useGMSDebugFile") +project.tasks.preBuild.dependsOn("generateSecretKey") + +android.sourceSets["main"].kotlin.srcDir(layout.buildDirectory.dir("generated/source/secrets")) diff --git a/app/src/main/java/com/capyreader/app/AddLinkActivity.kt b/app/src/main/java/com/capyreader/app/AddLinkActivity.kt index 37bf6d414..17e1d392c 100644 --- a/app/src/main/java/com/capyreader/app/AddLinkActivity.kt +++ b/app/src/main/java/com/capyreader/app/AddLinkActivity.kt @@ -42,7 +42,7 @@ class AddLinkActivity : BaseActivity() { AddLinkScreen( defaultQueryURL = defaultQueryURL, pageTitle = pageTitle, - supportsPages = account.source.supportsPages, + supportsReadLater = account.source.supportsReadLater, onBack = { finish() } diff --git a/app/src/main/java/com/capyreader/app/CommonModule.kt b/app/src/main/java/com/capyreader/app/CommonModule.kt index 6463da8be..16a795cf6 100644 --- a/app/src/main/java/com/capyreader/app/CommonModule.kt +++ b/app/src/main/java/com/capyreader/app/CommonModule.kt @@ -28,6 +28,8 @@ internal val common = module { clientCertManager = get(), userAgent = WebSettings.getDefaultUserAgent(androidContext()), acceptLanguage = Locale.getDefault().toAcceptLanguageTag(), + extractUsername = SecretKey.extractUsername, + extractSecret = SecretKey.extractSecret, ) } single { AppPreferences(get()) } diff --git a/app/src/main/java/com/capyreader/app/MainActivity.kt b/app/src/main/java/com/capyreader/app/MainActivity.kt index 79c0456eb..c9550f5d2 100644 --- a/app/src/main/java/com/capyreader/app/MainActivity.kt +++ b/app/src/main/java/com/capyreader/app/MainActivity.kt @@ -3,6 +3,9 @@ package com.capyreader.app import android.content.Intent import android.os.Bundle import androidx.activity.compose.setContent +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import com.capyreader.app.notifications.NotificationHelper import com.capyreader.app.preferences.AppPreferences import com.capyreader.app.ui.App @@ -13,21 +16,25 @@ import org.koin.android.ext.android.inject class MainActivity : BaseActivity() { val appPreferences by inject() + private var pendingArticleID by mutableStateOf(null) + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - NotificationHelper.openFromIntent(intent, appPreferences = appPreferences) + pendingArticleID = NotificationHelper.openFromIntent(intent, appPreferences = appPreferences) setContent { App( startDestination = startDestination(), appPreferences = appPreferences, + pendingArticleID = pendingArticleID, + onPendingArticleSelected = { pendingArticleID = null }, ) } } override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) - NotificationHelper.openFromIntent(intent, appPreferences = appPreferences) + pendingArticleID = NotificationHelper.openFromIntent(intent, appPreferences = appPreferences) } private fun startDestination(): Route { diff --git a/app/src/main/java/com/capyreader/app/notifications/NotificationHelper.kt b/app/src/main/java/com/capyreader/app/notifications/NotificationHelper.kt index 47793ff2b..04c385b63 100644 --- a/app/src/main/java/com/capyreader/app/notifications/NotificationHelper.kt +++ b/app/src/main/java/com/capyreader/app/notifications/NotificationHelper.kt @@ -155,7 +155,7 @@ class NotificationHelper( } } - fun openFromIntent(intent: Intent, appPreferences: AppPreferences) { + fun openFromIntent(intent: Intent, appPreferences: AppPreferences): String? { val openFromShowMore = intent.getBooleanExtra(UNREAD_ONLY_KEY, false) val articleID = intent.getStringExtra(ARTICLE_ID_KEY) val feedID = intent.getStringExtra(FEED_ID_KEY) @@ -167,24 +167,22 @@ class NotificationHelper( ArticleFilter.Articles(articleStatus = ArticleStatus.UNREAD) ) - appPreferences.articleID.delete() + return null } else if (articleID != null && feedID != null) { intent.replaceExtras(Bundle()) appPreferences.filter.getAndSet { currentFilter -> ArticleFilter.Feeds( feedID, - feedStatus = if (currentFilter.status != ArticleStatus.STARRED) { - currentFilter.status - } else { - ArticleStatus.UNREAD - }, + feedStatus = currentFilter.status, folderTitle = null ) } - appPreferences.articleID.set(articleID) + return articleID } + + return null } } } diff --git a/app/src/main/java/com/capyreader/app/preferences/AppPreferences.kt b/app/src/main/java/com/capyreader/app/preferences/AppPreferences.kt index 4abbc6030..c2cd5c745 100644 --- a/app/src/main/java/com/capyreader/app/preferences/AppPreferences.kt +++ b/app/src/main/java/com/capyreader/app/preferences/AppPreferences.kt @@ -51,9 +51,6 @@ class AppPreferences(context: Context) { val refreshInterval: Preference get() = preferenceStore.getEnum("refresh_interval", RefreshInterval.default) - val articleID: Preference - get() = preferenceStore.getString("article_id") - val crashReporting: Preference get() = preferenceStore.getBoolean("enable_crash_reporting", false) @@ -82,8 +79,19 @@ class AppPreferences(context: Context) { return preferenceStore.getBoolean("feed_group_${type.toString().lowercase()}", true) } - val showTodayFilter: Preference - get() = preferenceStore.getBoolean("show_today_filter", true) + val homePage: Preference + get() = preferenceStore.getObject( + key = "home_page", + defaultValue = HomePage.default, + serializer = { Json.encodeToString(it) }, + deserializer = { + try { + Json.decodeFromString(it) + } catch (e: Throwable) { + HomePage.default + } + } + ) val badgeStyle: Preference get() = preferenceStore.getEnum("badge_style", BadgeStyle.default) @@ -199,5 +207,8 @@ class AppPreferences(context: Context) { "after_read_all_behavior", AfterReadAllBehavior.default ) + + val hideReadArticles: Preference + get() = preferenceStore.getBoolean("article_list_hide_read", false) } } diff --git a/app/src/main/java/com/capyreader/app/preferences/HomePage.kt b/app/src/main/java/com/capyreader/app/preferences/HomePage.kt new file mode 100644 index 000000000..2170a5ca5 --- /dev/null +++ b/app/src/main/java/com/capyreader/app/preferences/HomePage.kt @@ -0,0 +1,41 @@ +package com.capyreader.app.preferences + +import com.jocmp.capy.ArticleFilter +import com.jocmp.capy.ArticleStatus +import kotlinx.serialization.Serializable + +@Serializable +sealed class HomePage { + @Serializable + data object Today : HomePage() + + @Serializable + data object Unread : HomePage() + + @Serializable + data object Starred : HomePage() + + @Serializable + data object ReadLater : HomePage() + + fun toArticleFilter(readLaterFeedID: String? = null): ArticleFilter { + return when (this) { + is Today -> ArticleFilter.Today(todayStatus = ArticleStatus.ALL) + is Unread -> ArticleFilter.Articles(articleStatus = ArticleStatus.UNREAD) + is Starred -> ArticleFilter.Starred() + is ReadLater -> if (readLaterFeedID != null) { + ArticleFilter.Feeds( + feedID = readLaterFeedID, + folderTitle = null, + feedStatus = ArticleStatus.ALL, + ) + } else { + ArticleFilter.Articles(articleStatus = ArticleStatus.UNREAD) + } + } + } + + companion object { + val default: HomePage = Today + } +} diff --git a/app/src/main/java/com/capyreader/app/ui/App.kt b/app/src/main/java/com/capyreader/app/ui/App.kt index bd5a4d548..79416d361 100644 --- a/app/src/main/java/com/capyreader/app/ui/App.kt +++ b/app/src/main/java/com/capyreader/app/ui/App.kt @@ -17,6 +17,8 @@ import com.capyreader.app.unloadAccountModules fun App( startDestination: Route, appPreferences: AppPreferences, + pendingArticleID: String? = null, + onPendingArticleSelected: () -> Unit = {}, ) { val navController = rememberNavController() @@ -54,7 +56,11 @@ fun App( unloadAccountModules() } ) - articleGraph(navController = navController) + articleGraph( + navController = navController, + pendingArticleID = pendingArticleID, + onPendingArticleSelected = onPendingArticleSelected, + ) } } } diff --git a/app/src/main/java/com/capyreader/app/ui/ArticleStatusNavigationTitleExt.kt b/app/src/main/java/com/capyreader/app/ui/ArticleStatusNavigationTitleExt.kt index fb472560c..d17863c43 100644 --- a/app/src/main/java/com/capyreader/app/ui/ArticleStatusNavigationTitleExt.kt +++ b/app/src/main/java/com/capyreader/app/ui/ArticleStatusNavigationTitleExt.kt @@ -7,5 +7,4 @@ val ArticleStatus.navigationTitle: Int get() = when (this) { ArticleStatus.ALL -> R.string.filter_all ArticleStatus.UNREAD -> R.string.filter_unread - ArticleStatus.STARRED -> R.string.filter_starred } diff --git a/app/src/main/java/com/capyreader/app/ui/SourceNavigationTitleExt.kt b/app/src/main/java/com/capyreader/app/ui/SourceNavigationTitleExt.kt index 9254dce9c..721269486 100644 --- a/app/src/main/java/com/capyreader/app/ui/SourceNavigationTitleExt.kt +++ b/app/src/main/java/com/capyreader/app/ui/SourceNavigationTitleExt.kt @@ -8,11 +8,4 @@ val Source.savedSearchNavTitle: Int R.string.freshrss_nav_headline_my_labels } else { R.string.nav_headline_saved_searches - } - -val Source.folderNavTitle: Int - get() = if (this == Source.FRESHRSS) { - R.string.freshrss_nav_headline_categories - } else { - R.string.nav_headline_tags - } + } \ No newline at end of file diff --git a/app/src/main/java/com/capyreader/app/ui/accounts/AuthFields.kt b/app/src/main/java/com/capyreader/app/ui/accounts/AuthFields.kt index 8da534eee..745d7bd3c 100644 --- a/app/src/main/java/com/capyreader/app/ui/accounts/AuthFields.kt +++ b/app/src/main/java/com/capyreader/app/ui/accounts/AuthFields.kt @@ -38,6 +38,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.autofill.ContentType +import androidx.compose.ui.autofill.contentType import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.platform.LocalSoftwareKeyboardController @@ -50,7 +51,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.capyreader.app.R import com.capyreader.app.ui.articles.feeds.IconDropdown -import androidx.compose.ui.autofill.contentType import com.capyreader.app.ui.theme.CapyTheme import com.jocmp.capy.accounts.Source @@ -114,6 +114,7 @@ fun AuthFields( ), modifier = Modifier .fillMaxWidth() + .contentType(ContentType.Username) .contentType(ContentType.EmailAddress) ) } diff --git a/app/src/main/java/com/capyreader/app/ui/addintent/AddLinkScreen.kt b/app/src/main/java/com/capyreader/app/ui/addintent/AddLinkScreen.kt index 7ad708758..fb59813af 100644 --- a/app/src/main/java/com/capyreader/app/ui/addintent/AddLinkScreen.kt +++ b/app/src/main/java/com/capyreader/app/ui/addintent/AddLinkScreen.kt @@ -49,14 +49,14 @@ fun AddLinkScreen( onBack: () -> Unit, defaultQueryURL: String, pageTitle: String, - supportsPages: Boolean, + supportsReadLater: Boolean, ) { val (successMessage, setSuccessMessage) = remember { mutableStateOf(null) } AddLinkView( defaultQueryURL = defaultQueryURL, pageTitle = pageTitle, - supportsPages = supportsPages, + supportsReadLater = supportsReadLater, onBack = onBack, feedChoices = viewModel.feedChoices, onSearchFeed = { url -> @@ -66,7 +66,6 @@ fun AddLinkScreen( viewModel.addFeed( url = url, onComplete = { - viewModel.selectFeed(it.id) setSuccessMessage("feed") }, ) @@ -92,7 +91,7 @@ fun AddLinkScreen( fun AddLinkView( defaultQueryURL: String, pageTitle: String = "", - supportsPages: Boolean, + supportsReadLater: Boolean, onBack: () -> Unit, feedChoices: List = emptyList(), onSearchFeed: (url: String) -> Unit = {}, @@ -104,7 +103,7 @@ fun AddLinkView( savePageError: String? = null, successMessage: String? = null, ) { - val defaultTab = if (supportsPages && defaultQueryURL.isNotBlank()) { + val defaultTab = if (supportsReadLater && defaultQueryURL.isNotBlank()) { AddLinkTab.SAVE_PAGE } else { AddLinkTab.ADD_FEED @@ -140,7 +139,7 @@ fun AddLinkView( Column( modifier = Modifier.navigationBarsPadding() ) { - if (supportsPages && successMessage == null) { + if (supportsReadLater && successMessage == null) { val tabs = AddLinkTab.entries Row( @@ -259,7 +258,7 @@ private enum class AddLinkTab(val title: Int, val icon: ImageVector) { private fun AddLinkViewPreview() { AddLinkView( defaultQueryURL = "", - supportsPages = true, + supportsReadLater = true, onBack = {}, ) } diff --git a/app/src/main/java/com/capyreader/app/ui/addintent/AddLinkViewModel.kt b/app/src/main/java/com/capyreader/app/ui/addintent/AddLinkViewModel.kt index e5548c6d8..ce1d8fc22 100644 --- a/app/src/main/java/com/capyreader/app/ui/addintent/AddLinkViewModel.kt +++ b/app/src/main/java/com/capyreader/app/ui/addintent/AddLinkViewModel.kt @@ -3,22 +3,18 @@ package com.capyreader.app.ui.addintent import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.capyreader.app.preferences.AppPreferences import com.jocmp.capy.Account -import com.jocmp.capy.ArticleFilter import com.jocmp.capy.Feed import com.jocmp.capy.accounts.AddFeedResult import com.jocmp.capy.accounts.FeedOption import com.jocmp.capy.accounts.Source import com.jocmp.capy.common.launchIO import com.jocmp.capy.common.withUIContext -import com.jocmp.capy.preferences.getAndSet import okio.IOException import java.net.UnknownHostException class AddLinkViewModel( private val account: Account, - private val appPreferences: AppPreferences, ) : ViewModel() { private val _feedResult = mutableStateOf(null) private val _feedLoading = mutableStateOf(false) @@ -90,12 +86,6 @@ class AddLinkViewModel( } } - fun selectFeed(id: String) { - appPreferences.filter.getAndSet { - ArticleFilter.Feeds(feedID = id, folderTitle = null, it.status) - } - } - private val _pageLoading = mutableStateOf(false) private val _pageError = mutableStateOf(null) diff --git a/app/src/main/java/com/capyreader/app/ui/articles/AddFeedViewModel.kt b/app/src/main/java/com/capyreader/app/ui/articles/AddFeedViewModel.kt index 84c679684..a654809d3 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/AddFeedViewModel.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/AddFeedViewModel.kt @@ -3,20 +3,16 @@ package com.capyreader.app.ui.articles import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.capyreader.app.preferences.AppPreferences import com.jocmp.capy.Account -import com.jocmp.capy.ArticleFilter import com.jocmp.capy.Feed import com.jocmp.capy.accounts.AddFeedResult import com.jocmp.capy.accounts.FeedOption import com.jocmp.capy.accounts.Source import com.jocmp.capy.common.launchIO import com.jocmp.capy.common.withUIContext -import com.jocmp.capy.preferences.getAndSet class AddFeedViewModel( val account: Account, - private val appPreferences: AppPreferences, ) : ViewModel() { private val _result = mutableStateOf(null) private val _loading = mutableStateOf(false) @@ -60,9 +56,4 @@ class AddFeedViewModel( } } - fun selectFeed(id: String) { - appPreferences.filter.getAndSet { - ArticleFilter.Feeds(feedID = id, folderTitle = null, it.status) - } - } } diff --git a/app/src/main/java/com/capyreader/app/ui/articles/ArticleActions.kt b/app/src/main/java/com/capyreader/app/ui/articles/ArticleActions.kt index 52988c729..c9015fe77 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/ArticleActions.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/ArticleActions.kt @@ -12,4 +12,6 @@ data class ArticleActions( val markUnread: (articleID: String) -> Unit = {}, val unstar: (articleID: String) -> Unit = {}, val saveExternally: (articleID: String, onComplete: (Result) -> Unit) -> Unit = { _, _ -> }, + val saveForLater: (url: String, onComplete: (Result) -> Unit) -> Unit = { _, _ -> }, + val showSaveForLater: Boolean = false, ) diff --git a/app/src/main/java/com/capyreader/app/ui/articles/ArticleDisplayFeedNameExt.kt b/app/src/main/java/com/capyreader/app/ui/articles/ArticleDisplayFeedNameExt.kt new file mode 100644 index 000000000..113ceb614 --- /dev/null +++ b/app/src/main/java/com/capyreader/app/ui/articles/ArticleDisplayFeedNameExt.kt @@ -0,0 +1,13 @@ +package com.capyreader.app.ui.articles + +import android.content.Context +import com.capyreader.app.R +import com.jocmp.capy.Article + +fun Article.displayFeedName(context: Context): String { + return if (isReadLater) { + context.getString(R.string.filter_read_later) + } else { + feedName + } +} diff --git a/app/src/main/java/com/capyreader/app/ui/articles/ArticleHandler.kt b/app/src/main/java/com/capyreader/app/ui/articles/ArticleHandler.kt deleted file mode 100644 index eb894648e..000000000 --- a/app/src/main/java/com/capyreader/app/ui/articles/ArticleHandler.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.capyreader.app.ui.articles - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import com.capyreader.app.preferences.AppPreferences -import com.jocmp.capy.Article -import org.koin.compose.koinInject - -@Composable -fun ArticleHandler( - article: Article?, - appPreferences: AppPreferences = koinInject(), - onRequestArticle: (articleID: String) -> Unit, -) { - LaunchedEffect(article?.id) { - if (article != null) { - return@LaunchedEffect - } - - val articleID = appPreferences.articleID.get() - - if (articleID.isNotBlank()) { - appPreferences.articleID.delete() - onRequestArticle(articleID) - } - } -} diff --git a/app/src/main/java/com/capyreader/app/ui/articles/ArticleList.kt b/app/src/main/java/com/capyreader/app/ui/articles/ArticleList.kt index 96d0ec887..873a4fec0 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/ArticleList.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/ArticleList.kt @@ -32,7 +32,6 @@ import androidx.paging.compose.itemKey import com.capyreader.app.R import com.capyreader.app.preferences.AppPreferences import com.jocmp.capy.Article -import com.jocmp.capy.ArticleStatus import com.jocmp.capy.MarkRead import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.delay @@ -49,10 +48,10 @@ fun ArticleList( onMarkAllRead: (range: MarkRead) -> Unit = {}, refreshingAll: Boolean, enableMarkReadOnScroll: Boolean = false, - filterStatus: ArticleStatus = ArticleStatus.ALL, + dimReadArticles: Boolean = true, ) { val articleOptions = rememberArticleOptions().copy( - dim = filterStatus != ArticleStatus.STARRED, + dim = dimReadArticles, ) val currentTime = rememberCurrentTime() val localDensity = LocalDensity.current diff --git a/app/src/main/java/com/capyreader/app/ui/articles/ArticleNavigation.kt b/app/src/main/java/com/capyreader/app/ui/articles/ArticleNavigation.kt index 2d3c78d5e..992b38dbe 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/ArticleNavigation.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/ArticleNavigation.kt @@ -7,9 +7,13 @@ import com.capyreader.app.ui.Route fun NavGraphBuilder.articleGraph( navController: NavController, + pendingArticleID: String? = null, + onPendingArticleSelected: () -> Unit = {}, ) { composable { ArticleScreen( + pendingArticleID = pendingArticleID, + onPendingArticleSelected = onPendingArticleSelected, onNavigateToSettings = { navController.navigate(Route.Settings) { launchSingleTop = true diff --git a/app/src/main/java/com/capyreader/app/ui/articles/ArticlePagerFactory.kt b/app/src/main/java/com/capyreader/app/ui/articles/ArticlePagerFactory.kt index e0ba3de3d..a1db6e400 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/ArticlePagerFactory.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/ArticlePagerFactory.kt @@ -26,6 +26,7 @@ class ArticlePagerFactory(private val database: Database) { is ArticleFilter.Folders -> folderSource(filter, query, sortOrder, since) is ArticleFilter.SavedSearches -> savedSearchSource(filter, query, sortOrder, since) is ArticleFilter.Today -> todaySource(filter, query, sortOrder, since) + is ArticleFilter.Starred -> starredSource(filter, query, sortOrder, since) } } @@ -157,6 +158,32 @@ class ArticlePagerFactory(private val database: Database) { ) } + private fun starredSource( + filter: ArticleFilter.Starred, + query: String?, + sortOrder: SortOrder, + since: OffsetDateTime + ): PagingSource { + return QueryPagingSource( + countQuery = articles.byStatus.countStarred( + status = filter.status, + query = query, + ), + transacter = database.articlesQueries, + context = Dispatchers.IO, + queryProvider = { limit, offset -> + articles.byStatus.allStarred( + status = filter.status, + query = query, + limit = limit, + sortOrder = sortOrder, + offset = offset, + since = since, + ) + } + ) + } + private fun todaySource( filter: ArticleFilter.Today, query: String?, diff --git a/app/src/main/java/com/capyreader/app/ui/articles/ArticlePaneExpansion.kt b/app/src/main/java/com/capyreader/app/ui/articles/ArticlePaneExpansion.kt index 3897cd4c5..26d10d29e 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/ArticlePaneExpansion.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/ArticlePaneExpansion.kt @@ -34,7 +34,6 @@ private val ArticlePaneAnchors: List = buildList { class ArticlePaneExpansion( val state: PaneExpansionState, val isFullscreen: Boolean, - val isDetailHidden: Boolean, private val anchors: List, private val lastAnchorIndex: Int, private val scope: CoroutineScope, @@ -107,7 +106,6 @@ fun rememberArticlePaneExpansion( ArticlePaneExpansion( state = paneExpansionState, isFullscreen = isFullscreen, - isDetailHidden = isDetailHidden, anchors = anchors, lastAnchorIndex = lastAnchorIndex, scope = scope, diff --git a/app/src/main/java/com/capyreader/app/ui/articles/ArticleRow.kt b/app/src/main/java/com/capyreader/app/ui/articles/ArticleRow.kt index 076a1828e..cd1b9c4ed 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/ArticleRow.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/ArticleRow.kt @@ -33,12 +33,14 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Stable 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.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.stringResource @@ -60,6 +62,7 @@ import com.capyreader.app.ui.articles.list.ArticleListItem import com.capyreader.app.ui.articles.list.ArticleRowSwipeBox import com.capyreader.app.ui.fixtures.ArticleSample import com.capyreader.app.ui.fixtures.PreviewKoinApplication +import com.capyreader.app.ui.components.LocalSnackbarHost import com.capyreader.app.ui.theme.CapyTheme import com.capyreader.app.ui.theme.LocalAppTheme import com.jocmp.capy.Article @@ -67,6 +70,7 @@ import com.jocmp.capy.EnclosureType import com.jocmp.capy.MarkRead import com.jocmp.capy.articles.relativeTime import java.net.URL +import kotlinx.coroutines.launch import java.time.LocalDateTime import java.time.ZoneOffset import java.time.ZonedDateTime @@ -108,6 +112,9 @@ fun ArticleRow( val haptics = LocalHapticFeedback.current val (isArticleMenuOpen, setArticleMenuOpen) = remember { mutableStateOf(false) } val labelsActions = LocalLabelsActions.current + val snackbarHost = LocalSnackbarHost.current + val scope = rememberCoroutineScope() + val savedForLaterMessage = stringResource(R.string.saved_for_later_success) val openArticleMenu = { haptics.performHapticFeedback(HapticFeedbackType.LongPress) setArticleMenuOpen(true) @@ -141,7 +148,7 @@ fun ArticleRow( if (options.showFeedName) { Text( - text = article.feedName, + text = article.displayFeedName(LocalContext.current), color = feedNameColor, maxLines = 1, overflow = TextOverflow.Ellipsis, @@ -223,11 +230,14 @@ fun ArticleRow( colors = colors ) + val articleActions = LocalArticleActions.current + ArticleActionMenu( expanded = isArticleMenuOpen, article = article, index = index, showLabels = labelsActions.showLabels, + showSaveForLater = articleActions.showSaveForLater, onMarkAllRead = { setArticleMenuOpen(false) onMarkAllRead(it) @@ -236,6 +246,14 @@ fun ArticleRow( setArticleMenuOpen(false) labelsActions.openSheet(article.id) }, + onSaveForLater = { url -> + setArticleMenuOpen(false) + articleActions.saveForLater(url) { result -> + if (result.isSuccess) { + scope.launch { snackbarHost.showSnackbar(savedForLaterMessage) } + } + } + }, onDismissRequest = { setArticleMenuOpen(false) } diff --git a/app/src/main/java/com/capyreader/app/ui/articles/ArticleScreen.kt b/app/src/main/java/com/capyreader/app/ui/articles/ArticleScreen.kt index c05607fec..c84a1895e 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/ArticleScreen.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/ArticleScreen.kt @@ -80,10 +80,9 @@ import com.capyreader.app.ui.components.SearchState import com.capyreader.app.ui.provideLinkOpener import com.capyreader.app.ui.rememberLazyListState import com.capyreader.app.ui.rememberLocalConnectivity -import com.capyreader.app.ui.settings.LocalSnackbarHost +import com.capyreader.app.ui.components.LocalSnackbarHost import com.jocmp.capy.Article import com.jocmp.capy.ArticleFilter -import com.jocmp.capy.ArticleStatus import com.jocmp.capy.Feed import com.jocmp.capy.Folder import com.jocmp.capy.MarkRead @@ -102,10 +101,12 @@ import org.koin.compose.koinInject fun ArticleScreen( viewModel: ArticleScreenViewModel = koinViewModel(), appPreferences: AppPreferences = koinInject(), + pendingArticleID: String? = null, + onPendingArticleSelected: () -> Unit = {}, onNavigateToSettings: () -> Unit, ) { val feeds by viewModel.topLevelFeeds.collectAsStateWithLifecycle(initialValue = emptyList()) - val pagesFeed by viewModel.pagesFeed.collectAsStateWithLifecycle(initialValue = null) + val readLaterFeed by viewModel.readLaterFeed.collectAsStateWithLifecycle(initialValue = null) val allFeeds by viewModel.allFeeds.collectAsStateWithLifecycle(initialValue = emptyList()) val allFolders by viewModel.allFolders.collectAsStateWithLifecycle(initialValue = emptyList()) val folders by viewModel.folders.collectAsStateWithLifecycle(initialValue = emptyList()) @@ -113,13 +114,14 @@ fun ArticleScreen( val allSavedSearches by viewModel.allSavedSearches.collectAsStateWithLifecycle(initialValue = emptyList()) val statusCount by viewModel.statusCount.collectAsStateWithLifecycle(initialValue = 0) val todayCount by viewModel.todayCount.collectAsStateWithLifecycle(initialValue = 0) + val starredCount by viewModel.starredCount.collectAsStateWithLifecycle(initialValue = 0) val unreadCount by viewModel.unreadCount.collectAsStateWithLifecycle(initialValue = 0L) - val showTodayFilter by viewModel.showTodayFilter.collectAsStateWithLifecycle(initialValue = true) - val filter by viewModel.filter.collectAsStateWithLifecycle(appPreferences.filter.get()) + val filter by viewModel.filter.collectAsStateWithLifecycle() val searchQuery by viewModel.searchQuery.collectAsStateWithLifecycle("") val searchState by viewModel.searchState.collectAsStateWithLifecycle(SearchState.INACTIVE) val nextFilter by viewModel.nextFilter.collectAsStateWithLifecycle(initialValue = null) val afterReadAll by viewModel.afterReadAll.collectAsStateWithLifecycle() + val hideReadArticles by viewModel.hideReadArticles.collectAsStateWithLifecycle() val scope = rememberCoroutineScope() val refreshInterval by appPreferences .refreshInterval @@ -205,7 +207,6 @@ fun ArticleScreen( val paneExpansion = rememberArticlePaneExpansion() var isPullToRefreshing by remember { mutableStateOf(false) } val addFeedSuccessMessage = stringResource(R.string.add_feed_success) - val currentFeed by viewModel.currentFeed.collectAsStateWithLifecycle(null) val scrollBehavior = pinnedScrollBehavior() var media by rememberSaveable(saver = Media.Saver) { mutableStateOf(null) } val audioController: AudioPlayerController = koinInject() @@ -427,10 +428,9 @@ fun ArticleScreen( } } - val selectStatus = { status: ArticleStatus -> - coroutineScope.launchUI { - openNextStatus { viewModel.selectStatus(status) } - } + val selectStarred = { + if (!filter.hasStarredSelected()) openNextList { viewModel.selectStarred() } + else closeDrawer() } val selectFeed = { feed: Feed, folderTitle: String? -> @@ -465,8 +465,10 @@ fun ArticleScreen( } } - ArticleHandler(article) { articleID -> - selectArticle(articleID) + LaunchedEffect(pendingArticleID) { + val id = pendingArticleID ?: return@LaunchedEffect + onPendingArticleSelected() + selectArticle(id) } ArticleScaffold( @@ -478,7 +480,7 @@ fun ArticleScreen( source = viewModel.source, folders = folders, feeds = feeds, - pagesFeed = pagesFeed, + readLaterFeed = readLaterFeed, onSelectFolder = selectFolder, onSelectFeed = selectFeed, onFeedAdded = { onFeedAdded(it) }, @@ -493,6 +495,7 @@ fun ArticleScreen( }, onFilterSelect = selectFilter, onSelectToday = { selectToday() }, + onSelectStarred = { selectStarred() }, refreshState = refreshAllState, onRefresh = { refreshAll() @@ -500,8 +503,7 @@ fun ArticleScreen( filter = filter, statusCount = statusCount, todayCount = todayCount, - showTodayFilter = showTodayFilter, - onSelectStatus = { selectStatus(it) } + starredCount = starredCount, ) }, listPane = { @@ -526,29 +528,17 @@ fun ArticleScreen( }), topBar = { ArticleListTopBar( - onRequestJumpToTop = { - scrollToTop() - }, - onNavigateToDrawer = { - openDrawer() - }, - onRemoveFolder = { folderTitle, completion -> - viewModel.removeFolder( - folderTitle, - completion - ) - }, + onRequestJumpToTop = { scrollToTop() }, + onNavigateToDrawer = { openDrawer() }, scrollBehavior = scrollBehavior, - onMarkAllRead = { - markAllRead(MarkRead.All) - }, + onMarkAllRead = { markAllRead(MarkRead.All) }, search = search, filter = filter, - currentFeed = currentFeed, feeds = allFeeds, savedSearches = savedSearches, folders = allFolders, - source = viewModel.source, + hideReadArticles = hideReadArticles, + onToggleHideReadArticles = { viewModel.toggleHideReadArticles() }, ) }, snackbarHost = { @@ -612,7 +602,7 @@ fun ArticleScreen( listState = listState, enableMarkReadOnScroll = enableMarkReadOnScroll, refreshingAll = viewModel.refreshingAll, - filterStatus = filter.status, + dimReadArticles = filter !is ArticleFilter.Starred, onMarkAllRead = { range -> onMarkAllRead(range) }, @@ -675,7 +665,8 @@ fun ArticleScreen( ) LaunchedEffect(scaffoldNavigator.currentDestination) { - val isOnList = scaffoldNavigator.currentDestination?.pane != ListDetailPaneScaffoldRole.Detail + val isOnList = + scaffoldNavigator.currentDestination?.pane != ListDetailPaneScaffoldRole.Detail if (isOnList && article != null) { viewModel.clearArticle() } @@ -780,6 +771,9 @@ fun rememberArticleActions(viewModel: ArticleScreenViewModel): ArticleActions { star = viewModel::addStarAsync, unstar = viewModel::removeStarAsync, saveExternally = viewModel::saveArticleExternallyAsync, + saveForLater = viewModel::saveForLater, + + showSaveForLater = viewModel.source.supportsReadLater, ) } } diff --git a/app/src/main/java/com/capyreader/app/ui/articles/ArticleScreenViewModel.kt b/app/src/main/java/com/capyreader/app/ui/articles/ArticleScreenViewModel.kt index 9d6ed58cb..8b7101c2c 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/ArticleScreenViewModel.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/ArticleScreenViewModel.kt @@ -21,7 +21,6 @@ import com.jocmp.capy.Account import com.jocmp.capy.Article import com.jocmp.capy.ArticleFilter import com.jocmp.capy.ArticleStatus -import com.jocmp.capy.ArticleStatus.STARRED import com.jocmp.capy.ArticleStatus.UNREAD import com.jocmp.capy.Feed import com.jocmp.capy.Folder @@ -29,7 +28,6 @@ import com.jocmp.capy.MarkRead import com.jocmp.capy.SavedSearch import com.jocmp.capy.articles.ArticleContent import com.jocmp.capy.articles.NextFilter -import com.capyreader.app.ui.articles.buildArticlePager import com.jocmp.capy.common.UnauthorizedError import com.jocmp.capy.common.launchIO import com.jocmp.capy.common.launchUI @@ -60,6 +58,9 @@ class ArticleScreenViewModel( val filter = appPreferences.filter.stateIn(viewModelScope) + val hideReadArticles = + appPreferences.articleListOptions.hideReadArticles.stateIn(viewModelScope) + private val listSwipeBottom = appPreferences.articleListOptions.swipeBottom.stateIn(viewModelScope) @@ -110,7 +111,6 @@ class ArticleScreenViewModel( filter, ) { folders, latestCounts, filter -> folders.map { copyFolderCounts(it, latestCounts, filter) } - .withPositiveCount(filter.status) } val savedSearches: Flow> = combine( @@ -119,7 +119,6 @@ class ArticleScreenViewModel( filter, ) { searches, latestCounts, filter -> searches.map { copySavedSearchCounts(it, latestCounts) } - .withPositiveCount(filter.status) } val allFeeds = account.taggedFeeds @@ -135,29 +134,20 @@ class ArticleScreenViewModel( _counts, filter, ) { feeds, latestCounts, filter -> - feeds.filter { !it.isPages } + feeds.filter { !it.isReadLater } .map { copyFeedCounts(it, latestCounts) } - .withPositiveCount(filter.status) } - val pagesFeed: Flow = combine( + val readLaterFeed: Flow = combine( account.feeds, _counts, filter, ) { feeds, latestCounts, filter -> - feeds.find { it.isPages } + feeds.find { it.isReadLater } ?.let { copyFeedCounts(it, latestCounts) } ?.takeIf { it.count > 0 || filter.status != ArticleStatus.UNREAD } } - val currentFeed: Flow = combine(allFeeds, filter) { feeds, filter -> - if (filter is ArticleFilter.Feeds) { - feeds.find { it.id == filter.feedID } - } else { - null - } - } - private val nextFilterListener: Flow = combine( listSwipeBottom, @@ -188,6 +178,8 @@ class ArticleScreenViewModel( account.countToday(countableStatus(filter)) } + val starredCount: Flow = account.countAllStarred() + val unreadCount: Flow = combine( filter, _searchQuery, @@ -197,7 +189,6 @@ class ArticleScreenViewModel( it } - val showTodayFilter: Flow = appPreferences.showTodayFilter.stateIn(viewModelScope) val showUnauthorizedMessage: Boolean get() = _showUnauthorizedMessage == UnauthorizedMessageState.SHOW @@ -223,58 +214,54 @@ class ArticleScreenViewModel( } fun selectArticleFilter() { - val filter = ArticleFilter.default().withStatus(status = latestFilter.status) - - updateFilter(filter) + updateFilter(ArticleFilter.Articles(articleStatus = UNREAD)) } - fun selectStatus(status: ArticleStatus) { - val filter = latestFilter.withStatus(status = status) + fun selectToday() { + updateFilter(ArticleFilter.Today(todayStatus = currentStatus)) + } - updateFilter(filter) + fun selectStarred() { + updateFilter(ArticleFilter.Starred(starredStatus = currentStatus)) } - fun selectToday() { - val filter = ArticleFilter.Today(todayStatus = latestFilter.status) + fun toggleHideReadArticles() { + val newValue = !hideReadArticles.value + appPreferences.articleListOptions.hideReadArticles.set(newValue) - updateFilter(filter) + val status = if (newValue) UNREAD else ArticleStatus.ALL + + updateFilter(latestFilter.withStatus(status)) } fun selectFeed(feedID: String, folderTitle: String? = null) { viewModelScope.launchIO { val feed = account.findFeed(feedID) ?: return@launchIO - val feedFilter = ArticleFilter.Feeds( - feedID = feed.id, - folderTitle = folderTitle, - feedStatus = latestFilter.status + updateFilter( + ArticleFilter.Feeds( + feedID = feed.id, + folderTitle = folderTitle, + feedStatus = currentStatus + ) ) - - updateFilter(feedFilter) } } fun selectSavedSearch(savedSearchID: String) { viewModelScope.launchIO { val savedSearch = account.findSavedSearch(savedSearchID) ?: return@launchIO - val searchFilter = ArticleFilter.SavedSearches( - savedSearch.id, - savedSearchStatus = latestFilter.status + updateFilter( + ArticleFilter.SavedSearches(savedSearch.id, savedSearchStatus = currentStatus) ) - - updateFilter(searchFilter) } } fun selectFolder(title: String) { viewModelScope.launchIO { val folder = account.findFolder(title) ?: return@launchIO - val feedFilter = - ArticleFilter.Folders( - folderTitle = folder.title, - folderStatus = latestFilter.status - ) - - updateFilter(feedFilter) + updateFilter( + ArticleFilter.Folders(folderTitle = folder.title, folderStatus = currentStatus) + ) } } @@ -328,6 +315,13 @@ class ArticleScreenViewModel( } } + fun saveForLater(url: String, onComplete: (Result) -> Unit) { + viewModelScope.launchIO { + val result = account.createPage(url) + withUIContext { onComplete(result) } + } + } + fun removeFeed( feedID: String, ) { @@ -407,8 +401,6 @@ class ArticleScreenViewModel( val article = buildArticle(articleID) ?: return@launchIO _article = article - appPreferences.articleID.set(articleID) - launchIO { markRead(articleID) } @@ -459,7 +451,6 @@ class ArticleScreenViewModel( fun clearArticle() { _article = null - appPreferences.articleID.delete() } fun startSearch() { @@ -548,7 +539,7 @@ class ArticleScreenViewModel( } private fun resetToDefaultFilter() { - updateFilter(ArticleFilter.default().copy(latestFilter.status)) + updateFilter(ArticleFilter.default().copy(currentStatus)) } private fun toggleCurrentStarred(articleID: String) { @@ -587,7 +578,7 @@ class ArticleScreenViewModel( val folderFeeds = folder.feeds.map { copyFeedCounts(it, counts) } return folder.copy( - feeds = folderFeeds.withPositiveCount(filter.status).toMutableList(), + feeds = folderFeeds, count = folderFeeds.sumOf { it.count } ) } @@ -612,10 +603,9 @@ class ArticleScreenViewModel( Article.FullContentState.NONE } - val content = if (fullContent == Article.FullContentState.LOADING) { - "" - } else { - article.defaultContent + val content = when (fullContent) { + Article.FullContentState.LOADING -> "" + else -> article.defaultContent } return article.copy( @@ -743,8 +733,9 @@ class ArticleScreenViewModel( } } - private val latestFilter: ArticleFilter - get() = filter.value + private val latestFilter: ArticleFilter get() = filter.value + private val currentStatus: ArticleStatus + get() = if (hideReadArticles.value) UNREAD else ArticleStatus.ALL private val enableStickyFullContent: Boolean get() = appPreferences.enableStickyFullContent.get() @@ -791,13 +782,11 @@ class ArticleScreenViewModel( } fun Context.showFullContentErrorToast(throwable: Throwable) { - val message = when { - throwable is ArticleContent.HttpError && throwable.code == 403 -> + val message = when (throwable) { + is ArticleContent.HttpError if throwable.code == 403 -> R.string.full_content_error_forbidden - throwable is ArticleContent.MissingBodyError -> - R.string.full_content_error_missing_response - + is ArticleContent.MissingBodyError -> R.string.full_content_error_missing_response else -> R.string.full_content_error_generic } @@ -805,9 +794,5 @@ fun Context.showFullContentErrorToast(throwable: Throwable) { } fun countableStatus(filter: ArticleFilter): ArticleStatus { - return if (filter.status == STARRED) { - STARRED - } else { - UNREAD - } + return UNREAD } diff --git a/app/src/main/java/com/capyreader/app/ui/articles/ArticleStatusBar.kt b/app/src/main/java/com/capyreader/app/ui/articles/ArticleStatusBar.kt index e2babcae1..05ac1cc37 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/ArticleStatusBar.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/ArticleStatusBar.kt @@ -43,7 +43,6 @@ fun ArticleStatusBar( val options = listOf( ArticleStatus.ALL, ArticleStatus.UNREAD, - ArticleStatus.STARRED, ) @Composable diff --git a/app/src/main/java/com/capyreader/app/ui/articles/ArticleStatusIcon.kt b/app/src/main/java/com/capyreader/app/ui/articles/ArticleStatusIcon.kt index 5443b374a..646415136 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/ArticleStatusIcon.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/ArticleStatusIcon.kt @@ -2,11 +2,9 @@ package com.capyreader.app.ui.articles import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.Notes -import androidx.compose.material.icons.rounded.Star +import androidx.compose.material.icons.rounded.FiberManualRecord import androidx.compose.material3.Icon import androidx.compose.runtime.Composable -import androidx.compose.ui.res.painterResource -import com.capyreader.app.R import com.jocmp.capy.ArticleStatus @Composable @@ -18,13 +16,8 @@ fun ArticleStatusIcon(status: ArticleStatus) { ) ArticleStatus.UNREAD -> Icon( - painterResource(R.drawable.icon_circle_filled), + Icons.Rounded.FiberManualRecord, contentDescription = null, ) - - ArticleStatus.STARRED -> Icon( - Icons.Rounded.Star, - contentDescription = null - ) } } diff --git a/app/src/main/java/com/capyreader/app/ui/articles/ArticlesModule.kt b/app/src/main/java/com/capyreader/app/ui/articles/ArticlesModule.kt index fb6020344..c7b0466c9 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/ArticlesModule.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/ArticlesModule.kt @@ -15,13 +15,11 @@ internal val articlesModule = module { factory { AddFeedViewModel( account = get(), - appPreferences = get() ) } factory { AddLinkViewModel( account = get(), - appPreferences = get() ) } single { @@ -62,13 +60,11 @@ internal val articlesModule = module { viewModel { EditFeedViewModel( account = get(), - appPreferences = get() ) } viewModel { EditFolderViewModel( account = get(), - appPreferences = get() ) } } diff --git a/app/src/main/java/com/capyreader/app/ui/articles/CountBadge.kt b/app/src/main/java/com/capyreader/app/ui/articles/CountBadge.kt index 2b6b74b52..a0cd7fa78 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/CountBadge.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/CountBadge.kt @@ -9,13 +9,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.capyreader.app.preferences.BadgeStyle import com.capyreader.app.ui.LocalBadgeStyle -import com.jocmp.capy.ArticleStatus @Composable fun CountBadge( count: Long, showBadge: Boolean = true, - status: ArticleStatus = ArticleStatus.ALL, ) { if (count < 1) { return @@ -24,7 +22,7 @@ fun CountBadge( when (LocalBadgeStyle.current) { BadgeStyle.EXACT -> Text(count.toString()) BadgeStyle.SIMPLE -> { - if (!showBadge || status == ArticleStatus.STARRED) { + if (!showBadge) { return } diff --git a/app/src/main/java/com/capyreader/app/ui/articles/EditFolderDialog.kt b/app/src/main/java/com/capyreader/app/ui/articles/EditFolderDialog.kt index 0e02309c7..5a6abf643 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/EditFolderDialog.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/EditFolderDialog.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.window.Dialog import com.capyreader.app.ui.components.DialogCard import com.jocmp.capy.EditFolderFormEntry +import com.jocmp.capy.Folder import kotlinx.coroutines.launch import org.koin.androidx.compose.koinViewModel @@ -14,7 +15,7 @@ fun EditFolderDialog( isOpen: Boolean, viewModel: EditFolderViewModel = koinViewModel(), onDismiss: () -> Unit, - completion: (result: Result) -> Unit, + completion: (result: Result) -> Unit, ) { val coroutineScope = rememberCoroutineScope() diff --git a/app/src/main/java/com/capyreader/app/ui/articles/EditFolderView.kt b/app/src/main/java/com/capyreader/app/ui/articles/EditFolderView.kt index a445217b7..a4975d074 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/EditFolderView.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/EditFolderView.kt @@ -40,7 +40,7 @@ fun EditFolderView( onValueChange = setTitle, placeholder = { Text(folderTitle) }, label = { - Text(stringResource(id = R.string.tag_name_title)) + Text(stringResource(id = R.string.folder_name_title)) }, keyboardOptions = KeyboardOptions( capitalization = KeyboardCapitalization.Words, diff --git a/app/src/main/java/com/capyreader/app/ui/articles/EditFolderViewModel.kt b/app/src/main/java/com/capyreader/app/ui/articles/EditFolderViewModel.kt index 8c73fa1c8..90d66a5f9 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/EditFolderViewModel.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/EditFolderViewModel.kt @@ -1,43 +1,20 @@ package com.capyreader.app.ui.articles import androidx.lifecycle.ViewModel -import com.capyreader.app.preferences.AppPreferences import com.jocmp.capy.Account -import com.jocmp.capy.ArticleFilter import com.jocmp.capy.EditFolderFormEntry +import com.jocmp.capy.Folder import com.jocmp.capy.common.withIOContext -import com.jocmp.capy.preferences.getAndSet class EditFolderViewModel( private val account: Account, - private val appPreferences: AppPreferences ) : ViewModel() { suspend fun submit( form: EditFolderFormEntry, - ): Result { + ): Result { return withIOContext { - account - .editFolder(form = form) - .fold( - onSuccess = { folder -> - appPreferences.filter.getAndSet { filter -> - if (filter is ArticleFilter.Folders) { - ArticleFilter.Folders( - folderTitle = folder.title, - filter.status - ) - } else { - filter - } - } - - Result.success(Unit) - }, - onFailure = { error -> - Result.failure(error) - } - ) + account.editFolder(form = form) } } } diff --git a/app/src/main/java/com/capyreader/app/ui/articles/FeedDisplayTitleExt.kt b/app/src/main/java/com/capyreader/app/ui/articles/FeedDisplayTitleExt.kt new file mode 100644 index 000000000..117ecfa1a --- /dev/null +++ b/app/src/main/java/com/capyreader/app/ui/articles/FeedDisplayTitleExt.kt @@ -0,0 +1,15 @@ +package com.capyreader.app.ui.articles + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.capyreader.app.R +import com.jocmp.capy.Feed + +@Composable +fun Feed.displayTitle(): String { + return if (isReadLater) { + stringResource(R.string.filter_read_later) + } else { + title + } +} diff --git a/app/src/main/java/com/capyreader/app/ui/articles/FilterActionMenu.kt b/app/src/main/java/com/capyreader/app/ui/articles/FilterActionMenu.kt index 6803d821c..53d5693da 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/FilterActionMenu.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/FilterActionMenu.kt @@ -1,52 +1,38 @@ package com.capyreader.app.ui.articles -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.outlined.FilterAlt +import androidx.compose.material.icons.rounded.FilterAlt import androidx.compose.material.icons.rounded.Search import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp import com.capyreader.app.R import com.capyreader.app.ui.LocalMarkAllReadButtonPosition -import com.capyreader.app.ui.articles.list.FeedActionMenu -import com.capyreader.app.ui.articles.list.FolderActionMenu import com.capyreader.app.ui.articles.list.MarkAllReadButton +import com.capyreader.app.ui.components.ToolbarTooltip import com.capyreader.app.ui.fixtures.FeedSample +import com.capyreader.app.ui.fixtures.PreviewKoinApplication import com.jocmp.capy.ArticleFilter import com.jocmp.capy.ArticleStatus import com.jocmp.capy.Feed -import com.jocmp.capy.accounts.Source @Composable fun FilterActionMenu( filter: ArticleFilter, - currentFeed: Feed?, onMarkAllRead: () -> Unit, - onRemoveFolder: (folderTitle: String, completion: (result: Result) -> Unit) -> Unit, onRequestSearch: () -> Unit, hideSearchIcon: Boolean, - source: Source, + hideReadArticles: Boolean = false, + onToggleHideReadArticles: () -> Unit = {}, ) { val markReadPosition = LocalMarkAllReadButtonPosition.current - val (expanded, setMenuExpanded) = remember(filter) { mutableStateOf(false) } - - val closeMenu = { - setMenuExpanded(false) - } Row { - Spacer(Modifier.width(48.dp)) if (!hideSearchIcon) { IconButton(onClick = onRequestSearch) { Icon( @@ -56,42 +42,33 @@ fun FilterActionMenu( } } - if (markReadPosition == MarkReadPosition.TOOLBAR) { - MarkAllReadButton( - onMarkAllRead = { - onMarkAllRead() - }, - ) - } + if (filter !is ArticleFilter.Articles) { + val tooltip = if (hideReadArticles) { + stringResource(R.string.article_list_show_read) + } else { + stringResource(R.string.article_list_hide_read) + } - Box { - if ((currentFeed != null && !currentFeed.isPages) || filter is ArticleFilter.Folders) { - IconButton(onClick = { setMenuExpanded(true) }) { + ToolbarTooltip(message = tooltip) { + IconButton(onClick = onToggleHideReadArticles) { Icon( - imageVector = Icons.Filled.MoreVert, - contentDescription = stringResource(R.string.filter_action_menu_description) + imageVector = if (hideReadArticles) { + Icons.Rounded.FilterAlt + } else { + Icons.Outlined.FilterAlt + }, + contentDescription = tooltip, ) } } + } - if (currentFeed != null && !currentFeed.isPages) { - FeedActionMenu( - expanded = expanded, - feed = currentFeed, - onDismissMenuRequest = { closeMenu() }, - source = source, - ) - } - - if (filter is ArticleFilter.Folders) { - FolderActionMenu( - expanded = expanded, - folderTitle = filter.folderTitle, - onDismissMenuRequest = { closeMenu() }, - onRemoveRequest = onRemoveFolder, - source = source, - ) - } + if (markReadPosition == MarkReadPosition.TOOLBAR) { + MarkAllReadButton( + onMarkAllRead = { + onMarkAllRead() + }, + ) } } } @@ -99,17 +76,35 @@ fun FilterActionMenu( @Preview @Composable fun FeedActionsPreview(@PreviewParameter(FeedSample::class) feed: Feed) { - FilterActionMenu( - onRemoveFolder = { _, _ -> }, - onMarkAllRead = {}, - onRequestSearch = {}, - currentFeed = feed, - filter = ArticleFilter.Feeds( - feedID = feed.id, - folderTitle = null, - feedStatus = ArticleStatus.ALL - ), - hideSearchIcon = false, - source = Source.LOCAL, - ) + PreviewKoinApplication { + FilterActionMenu( + onMarkAllRead = {}, + onRequestSearch = {}, + filter = ArticleFilter.Feeds( + feedID = feed.id, + folderTitle = null, + feedStatus = ArticleStatus.ALL + ), + hideReadArticles = true, + hideSearchIcon = true, + ) + } +} + +@Preview +@Composable +fun FeedActionsPreviewFilterOff(@PreviewParameter(FeedSample::class) feed: Feed) { + PreviewKoinApplication { + FilterActionMenu( + onMarkAllRead = {}, + onRequestSearch = {}, + filter = ArticleFilter.Feeds( + feedID = feed.id, + folderTitle = null, + feedStatus = ArticleStatus.ALL + ), + hideReadArticles = false, + hideSearchIcon = true, + ) + } } diff --git a/app/src/main/java/com/capyreader/app/ui/articles/FilterAppBarTitle.kt b/app/src/main/java/com/capyreader/app/ui/articles/FilterAppBarTitle.kt index bba2182f9..7e1968e68 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/FilterAppBarTitle.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/FilterAppBarTitle.kt @@ -16,7 +16,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import com.capyreader.app.R -import com.capyreader.app.ui.navigationTitle import com.jocmp.capy.ArticleFilter import com.jocmp.capy.Feed import com.jocmp.capy.Folder @@ -31,9 +30,9 @@ fun FilterAppBarTitle( onRequestJumpToTop: () -> Unit ) { val text = when (filter) { - is ArticleFilter.Articles -> stringResource(filter.articleStatus.navigationTitle) + is ArticleFilter.Articles -> stringResource(R.string.filter_unread) is ArticleFilter.Feeds -> { - allFeeds.find { it.id == filter.feedID }?.title + allFeeds.find { it.id == filter.feedID }?.displayTitle() } is ArticleFilter.Folders -> { @@ -44,6 +43,7 @@ fun FilterAppBarTitle( allSavedSearches.find { it.id == filter.savedSearchID }?.name is ArticleFilter.Today -> stringResource(R.string.filter_today) + is ArticleFilter.Starred -> stringResource(R.string.filter_starred) }.orEmpty() Box( diff --git a/app/src/main/java/com/capyreader/app/ui/articles/RemoveFolderDialog.kt b/app/src/main/java/com/capyreader/app/ui/articles/RemoveFolderDialog.kt index f00fae990..1585b05f7 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/RemoveFolderDialog.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/RemoveFolderDialog.kt @@ -13,9 +13,9 @@ fun RemoveFolderDialog( onConfirm: () -> Unit, onDismissRequest: () -> Unit ) { - val title = stringResource(R.string.tag_action_delete_title) - val message = stringResource(R.string.tag_action_delete_message, folderTitle) - val confirmText = stringResource(R.string.tag_action_delete_confirm) + val title = stringResource(R.string.folder_action_delete_title) + val message = stringResource(R.string.folder_action_delete_message, folderTitle) + val confirmText = stringResource(R.string.folder_action_delete_confirm) AlertDialog( onDismissRequest = onDismissRequest, diff --git a/app/src/main/java/com/capyreader/app/ui/articles/SavedSearchRow.kt b/app/src/main/java/com/capyreader/app/ui/articles/SavedSearchRow.kt index e419a1146..e59c050f5 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/SavedSearchRow.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/SavedSearchRow.kt @@ -8,7 +8,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.tooling.preview.Preview import com.capyreader.app.ui.articles.feeds.DrawerItem import com.capyreader.app.ui.articles.list.SavedSearchActionMenu -import com.jocmp.capy.ArticleStatus import com.jocmp.capy.SavedSearch @Composable @@ -16,7 +15,6 @@ fun SavedSearchRow( onSelect: (savedSearch: SavedSearch) -> Unit, selected: Boolean, savedSearch: SavedSearch, - status: ArticleStatus = ArticleStatus.ALL, ) { val (showMenu, setShowMenu) = remember { mutableStateOf(false) } @@ -24,7 +22,7 @@ fun SavedSearchRow( DrawerItem( label = { ListTitle(savedSearch.name) }, badge = { - CountBadge(count = savedSearch.count, showBadge = savedSearch.showUnreadBadge, status = status) + CountBadge(count = savedSearch.count, showBadge = savedSearch.showUnreadBadge) }, selected = selected, onClick = { diff --git a/app/src/main/java/com/capyreader/app/ui/articles/WithPositiveCount.kt b/app/src/main/java/com/capyreader/app/ui/articles/WithPositiveCount.kt deleted file mode 100644 index 3e64a6257..000000000 --- a/app/src/main/java/com/capyreader/app/ui/articles/WithPositiveCount.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.capyreader.app.ui.articles - -import com.jocmp.capy.ArticleStatus -import com.jocmp.capy.Countable - -fun List.withPositiveCount(status: ArticleStatus): List { - return filter { status == ArticleStatus.ALL || it.count > 0 } -} diff --git a/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleActions.kt b/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleActions.kt index 1b55e9d7a..599d12a6d 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleActions.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleActions.kt @@ -4,9 +4,9 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.Label -import androidx.compose.material.icons.outlined.Circle import androidx.compose.material.icons.outlined.FormatSize -import androidx.compose.material.icons.rounded.Circle +import androidx.compose.material.icons.outlined.FiberManualRecord +import androidx.compose.material.icons.rounded.FiberManualRecord import androidx.compose.material.icons.rounded.Share import androidx.compose.material.icons.rounded.Star import androidx.compose.material.icons.rounded.StarOutline @@ -140,9 +140,9 @@ fun ArticleActions( @Composable private fun readIcon(article: Article) = if (article.read) { - Icons.Outlined.Circle + Icons.Outlined.FiberManualRecord } else { - Icons.Rounded.Circle + Icons.Rounded.FiberManualRecord } @Composable diff --git a/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleBottomBar.kt b/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleBottomBar.kt index 1ca5b56a1..1ebb04e69 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleBottomBar.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleBottomBar.kt @@ -15,9 +15,9 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Circle -import androidx.compose.material.icons.rounded.Circle +import androidx.compose.material.icons.outlined.FiberManualRecord import androidx.compose.material.icons.rounded.ExpandMore +import androidx.compose.material.icons.rounded.FiberManualRecord import androidx.compose.material.icons.rounded.Share import androidx.compose.material.icons.rounded.Star import androidx.compose.material.icons.rounded.StarOutline @@ -98,7 +98,7 @@ fun ArticleBottomBar( onClick = { onToggleRead() }, ) { Icon( - if (article.read) Icons.Outlined.Circle else Icons.Rounded.Circle, + if (article.read) Icons.Outlined.FiberManualRecord else Icons.Rounded.FiberManualRecord, contentDescription = stringResource(R.string.article_view_mark_as_read), modifier = Modifier.size(24.dp) ) diff --git a/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleReader.kt b/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleReader.kt index 72f3d501d..5cb06a91f 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleReader.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleReader.kt @@ -39,7 +39,7 @@ import com.capyreader.app.ui.components.WebView import com.capyreader.app.ui.components.WebViewState import com.capyreader.app.ui.components.rememberSaveableShareLink import com.capyreader.app.ui.components.rememberWebViewState -import com.capyreader.app.ui.settings.LocalSnackbarHost +import com.capyreader.app.ui.components.LocalSnackbarHost import com.jocmp.capy.Article import com.jocmp.capy.common.launchIO import com.jocmp.capy.common.launchUI diff --git a/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleTopBar.kt b/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleTopBar.kt index b9ba79b42..2c837e155 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleTopBar.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleTopBar.kt @@ -47,7 +47,7 @@ import com.capyreader.app.ui.articles.LocalArticleActions import com.capyreader.app.ui.articles.LocalLabelsActions import com.capyreader.app.ui.components.ToolbarTooltip import com.capyreader.app.ui.fixtures.PreviewKoinApplication -import com.capyreader.app.ui.settings.LocalSnackbarHost +import com.capyreader.app.ui.components.LocalSnackbarHost import kotlinx.coroutines.launch private val sizeSpec = spring(stiffness = 700f) diff --git a/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleView.kt b/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleView.kt index 398100ff1..6eeabd6cf 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleView.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleView.kt @@ -43,7 +43,7 @@ import com.capyreader.app.ui.LocalLinkOpener import com.capyreader.app.ui.articles.LocalFullContent import com.capyreader.app.ui.collectChangesWithDefault import com.capyreader.app.ui.components.pullrefresh.SwipeRefresh -import com.capyreader.app.ui.settings.LocalSnackbarHost +import com.capyreader.app.ui.components.LocalSnackbarHost import com.jocmp.capy.Article import org.koin.compose.koinInject @@ -182,7 +182,7 @@ fun ArticleView( show = showToolBar, isScrolled = scrollState.showTopDivider, articleId = article.id, - canDeletePage = article.isPages, + canDeletePage = article.isReadLater, canSaveExternally = canSaveExternally, onDeletePage = onDeletePage, isFullscreen = isFullscreen, diff --git a/app/src/main/java/com/capyreader/app/ui/articles/feeds/FeedList.kt b/app/src/main/java/com/capyreader/app/ui/articles/feeds/FeedList.kt index 90b457088..c01cc7523 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/feeds/FeedList.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/feeds/FeedList.kt @@ -12,34 +12,30 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Bookmark import androidx.compose.material.icons.rounded.Refresh import androidx.compose.material.icons.rounded.Settings +import androidx.compose.material.icons.rounded.Star import androidx.compose.material.icons.rounded.Today import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.capyreader.app.R import com.capyreader.app.common.FeedGroup import com.capyreader.app.ui.articles.AddFeedButton -import com.capyreader.app.ui.articles.ArticleStatusBar import com.capyreader.app.ui.articles.ArticleStatusIcon import com.capyreader.app.ui.articles.CountBadge import com.capyreader.app.ui.articles.ListTitle import com.capyreader.app.ui.articles.SavedSearchRow -import com.capyreader.app.ui.fixtures.FeedSample -import com.capyreader.app.ui.fixtures.FolderPreviewFixture import com.capyreader.app.ui.fixtures.PreviewKoinApplication -import com.capyreader.app.ui.folderNavTitle -import com.capyreader.app.ui.navigationTitle import com.capyreader.app.ui.savedSearchNavTitle import com.capyreader.app.ui.theme.CapyTheme import com.jocmp.capy.ArticleFilter @@ -55,23 +51,22 @@ fun FeedList( filter: ArticleFilter, statusCount: Long, todayCount: Long, - showTodayFilter: Boolean = true, + starredCount: Long, folders: List = emptyList(), feeds: List = emptyList(), - pagesFeed: Feed? = null, + readLaterFeed: Feed? = null, savedSearches: List = emptyList(), onFilterSelect: () -> Unit, onSelectToday: () -> Unit, + onSelectStarred: () -> Unit, onSelectSavedSearch: (search: SavedSearch) -> Unit, refreshState: AngleRefreshState, onRefresh: () -> Unit, onSelectFolder: (folder: Folder) -> Unit, onSelectFeed: (feed: Feed, folderTitle: String?) -> Unit, onFeedAdded: (feedID: String) -> Unit, - onSelectStatus: (status: ArticleStatus) -> Unit, onNavigateToSettings: () -> Unit, ) { - val articleStatus = filter.status val scrollState = rememberScrollState() val buttonState = rememberRefreshButtonState(refreshState) @@ -91,14 +86,13 @@ fun FeedList( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - Text( - stringResource(R.string.feed_nav_drawer_title), - style = MaterialTheme.typography.titleLarge, - modifier = Modifier - .padding( - vertical = 18.dp, - horizontal = 12.dp - ), + Icon( + painterResource(R.drawable.capy_icon_small), + contentDescription = null, + modifier = Modifier.padding( + vertical = 18.dp, + horizontal = 16.dp + ), ) Row( horizontalArrangement = Arrangement.spacedBy(8.dp), @@ -110,9 +104,7 @@ fun FeedList( contentDescription = stringResource(R.string.settings) ) } - IconButton(onClick = { - onRefresh() - }) { + IconButton(onClick = { onRefresh() }) { Icon( imageVector = Icons.Rounded.Refresh, contentDescription = stringResource(R.string.feed_nav_drawer_refresh_all), @@ -123,57 +115,83 @@ fun FeedList( } AddFeedButton( iconOnly = true, - onComplete = { - onFeedAdded(it) - } + onComplete = { onFeedAdded(it) } ) } } DrawerItem( - icon = { ArticleStatusIcon(status = articleStatus) }, + icon = { + Icon( + Icons.Rounded.Today, + contentDescription = null + ) + }, + label = { + ListTitle( + stringResource(R.string.filter_today), + ) + }, + badge = { CountBadge(count = todayCount) }, + selected = filter.hasTodaySelected(), + onClick = { + onSelectToday() + } + ) + + DrawerItem( + icon = { ArticleStatusIcon(status = ArticleStatus.UNREAD) }, label = { ListTitle( - stringResource(articleStatus.navigationTitle), + stringResource(R.string.filter_unread), ) }, - badge = { CountBadge(count = statusCount, status = articleStatus) }, + badge = { CountBadge(count = statusCount) }, selected = filter.hasArticlesSelected(), onClick = { onFilterSelect() } ) - if (showTodayFilter) { + DrawerItem( + icon = { + Icon( + Icons.Rounded.Star, + contentDescription = null + ) + }, + label = { + ListTitle( + stringResource(R.string.filter_starred), + ) + }, + badge = { CountBadge(count = starredCount) }, + selected = filter.hasStarredSelected(), + onClick = { + onSelectStarred() + } + ) + + if (readLaterFeed != null) { DrawerItem( icon = { Icon( - Icons.Rounded.Today, + Icons.Rounded.Bookmark, contentDescription = null ) }, label = { ListTitle( - stringResource(R.string.filter_today), + stringResource(R.string.filter_read_later), ) }, - badge = { CountBadge(count = todayCount, status = articleStatus) }, - selected = filter.hasTodaySelected(), + badge = { CountBadge(count = readLaterFeed.count) }, + selected = filter.isFeedSelected(readLaterFeed), onClick = { - onSelectToday() + onSelectFeed(readLaterFeed, null) } ) } - if (pagesFeed != null) { - FeedRow( - feed = pagesFeed, - onSelect = { onSelectFeed(it, null) }, - selected = filter.isFeedSelected(pagesFeed), - status = articleStatus, - showContextMenu = false, - ) - } - Spacer(Modifier.height(8.dp)) if (savedSearches.isNotEmpty()) { @@ -187,7 +205,6 @@ fun FeedList( onSelect = onSelectSavedSearch, selected = filter.isSavedSearchSelected(it), savedSearch = it, - status = articleStatus, ) } } @@ -197,7 +214,7 @@ fun FeedList( FeedListDivider() FeedGroupList( type = FeedGroup.FOLDERS, - title = stringResource(source.folderNavTitle) + title = stringResource(R.string.nav_headline_folders) ) { folders.forEach { folder -> FolderRow( @@ -226,7 +243,6 @@ fun FeedList( onSelectFeed(it, null) }, selected = filter.isFeedSelected(feed), - status = articleStatus, source = source, ) } @@ -235,20 +251,6 @@ fun FeedList( Box(Modifier.padding(vertical = 16.dp)) } - - HorizontalDivider() - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 12.dp, vertical = 8.dp), - horizontalArrangement = Arrangement.Center - ) { - ArticleStatusBar( - status = filter.status, - onSelectStatus = onSelectStatus, - ) - } } } @@ -260,27 +262,22 @@ private fun FeedListDivider() { @Preview @Composable fun FeedListPreview() { - val folders = FolderPreviewFixture().values.take(2).toList() - val feeds = FeedSample().values.take(2).toList() - PreviewKoinApplication { CapyTheme { FeedList( source = Source.LOCAL, - folders = folders, - feeds = feeds, onSelectFolder = {}, onSelectFeed = { _, _ -> }, onNavigateToSettings = {}, onRefresh = {}, onFilterSelect = {}, onSelectToday = {}, + onSelectStarred = {}, filter = ArticleFilter.default(), statusCount = 10, todayCount = 5, - showTodayFilter = true, + starredCount = 3, onFeedAdded = {}, - onSelectStatus = {}, onSelectSavedSearch = {}, refreshState = AngleRefreshState.STOPPED, ) diff --git a/app/src/main/java/com/capyreader/app/ui/articles/feeds/FeedRow.kt b/app/src/main/java/com/capyreader/app/ui/articles/feeds/FeedRow.kt index c8aa97823..5ad35530e 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/feeds/FeedRow.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/feeds/FeedRow.kt @@ -11,7 +11,6 @@ import com.capyreader.app.ui.articles.FaviconBadge import com.capyreader.app.ui.articles.ListTitle import com.capyreader.app.ui.articles.list.FeedActionMenu import com.capyreader.app.ui.fixtures.FeedSample -import com.jocmp.capy.ArticleStatus import com.jocmp.capy.Feed import com.jocmp.capy.accounts.Source @@ -20,7 +19,6 @@ fun FeedRow( selected: Boolean, feed: Feed, onSelect: (feed: Feed) -> Unit, - status: ArticleStatus = ArticleStatus.ALL, showContextMenu: Boolean = true, source: Source = Source.LOCAL, ) { @@ -33,7 +31,7 @@ fun FeedRow( }, label = { ListTitle(feed.title) }, badge = { - CountBadge(count = feed.count, showBadge = feed.showUnreadBadge, status = status) + CountBadge(count = feed.count, showBadge = feed.showUnreadBadge) }, selected = selected, onClick = { diff --git a/app/src/main/java/com/capyreader/app/ui/articles/feeds/FolderRow.kt b/app/src/main/java/com/capyreader/app/ui/articles/feeds/FolderRow.kt index b601cbe2d..4f37a48ed 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/feeds/FolderRow.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/feeds/FolderRow.kt @@ -54,7 +54,7 @@ fun FolderRow( onClick = { onFolderSelect(folder) }, onLongClick = { setShowMenu(true) }, badge = { - CountBadge(count = folder.count, showBadge = showFolderBadge, status = filter.status) + CountBadge(count = folder.count, showBadge = showFolderBadge) }, icon = { IconDropdown( @@ -89,7 +89,6 @@ fun FolderRow( feed = feed, onSelect = { onFeedSelect(feed) }, selected = filter.isFeedSelected(feed), - status = filter.status, source = source, ) } diff --git a/app/src/main/java/com/capyreader/app/ui/articles/feeds/edit/EditFeedView.kt b/app/src/main/java/com/capyreader/app/ui/articles/feeds/edit/EditFeedView.kt index 45497f7ee..df90e5389 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/feeds/edit/EditFeedView.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/feeds/edit/EditFeedView.kt @@ -17,16 +17,12 @@ import androidx.compose.material.icons.rounded.Add import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.Checkbox -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExposedDropdownMenuAnchorType.Companion.PrimaryNotEditable -import androidx.compose.material3.ExposedDropdownMenuBox -import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.RadioButton import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -35,6 +31,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.toMutableStateMap import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardCapitalization @@ -145,7 +143,7 @@ fun EditFeedView( if (showMultiselect) { FormSection( modifier = Modifier.padding(bottom = 16.dp), - title = stringResource(R.string.edit_feed_tags_section) + title = stringResource(R.string.edit_feed_folders_section) ) { FolderMultiselect( folders, @@ -161,15 +159,14 @@ fun EditFeedView( ) } } else { - Column( - Modifier - .padding(horizontal = 16.dp) - .padding(bottom = 16.dp) + FormSection( + modifier = Modifier.padding(bottom = 16.dp), + title = stringResource(R.string.edit_feed_folders_section) ) { - FolderSelect( - onChange = setSelectedFolder, - value = selectedFolder, - options = folders.map { it.title } + FolderRadioSelect( + folders = folders, + selectedFolder = selectedFolder, + onSelectFolder = setSelectedFolder, ) } } @@ -192,44 +189,70 @@ fun EditFeedView( } } -@OptIn(ExperimentalMaterial3Api::class) @Composable -private fun FolderSelect( - options: List, - onChange: (value: String) -> Unit, - value: String, +private fun FolderRadioSelect( + folders: List, + selectedFolder: String, + onSelectFolder: (String) -> Unit, ) { - val (expanded, setExpanded) = remember { mutableStateOf(false) } + val (newFolderText, setNewFolderText) = remember { mutableStateOf("") } + val (isFocused, setFocused) = remember { mutableStateOf(false) } + val previousFolder = remember { mutableStateOf(selectedFolder) } + val focusManager = LocalFocusManager.current - ExposedDropdownMenuBox( - expanded = expanded, - onExpandedChange = { setExpanded(it) }, - ) { - OutlinedTextField( - modifier = Modifier - .menuAnchor(PrimaryNotEditable) - .fillMaxWidth(), - value = value, - onValueChange = onChange, - label = { Text(stringResource(R.string.edit_feed_tag_section)) }, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, - colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(), - ) - ExposedDropdownMenu( - expanded = expanded, - onDismissRequest = { setExpanded(false) } + Column { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, ) { - options.forEach { option -> - DropdownMenuItem( - text = { - Text(text = option) - }, - onClick = { - onChange(option) - setExpanded(false) + OutlinedTextField( + value = newFolderText, + onValueChange = { value -> + setNewFolderText(value) + if (value.isNotBlank()) { + onSelectFolder(value) + } else { + onSelectFolder(previousFolder.value) } - ) - } + }, + leadingIcon = { + Icon( + imageVector = Icons.Rounded.Add, + contentDescription = stringResource(R.string.filters_add_keyword) + ) + }, + label = { Text(stringResource(id = R.string.add_feed_new_folder_title)) }, + singleLine = true, + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.Words, + autoCorrectEnabled = false, + imeAction = ImeAction.Done + ), + modifier = Modifier + .weight(1f) + .padding(horizontal = 16.dp) + .padding(bottom = 8.dp) + .fillMaxWidth() + .onFocusChanged { state -> + if (state.isFocused && !isFocused) { + previousFolder.value = selectedFolder + } + setFocused(state.isFocused) + } + ) + } + + folders.forEach { folder -> + RadioRow( + title = folder.title, + selected = !isFocused && selectedFolder == folder.title, + onClick = { + focusManager.clearFocus() + setNewFolderText("") + previousFolder.value = folder.title + onSelectFolder(folder.title) + } + ) } } } @@ -300,6 +323,29 @@ private fun FolderMultiselect( } } +@Composable +private fun RadioRow(title: String, selected: Boolean, onClick: () -> Unit) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable { onClick() } + ) { + RadioButton( + selected = selected, + onClick = onClick, + modifier = Modifier.padding(start = 8.dp) + ) + Text( + text = title, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(end = 16.dp) + ) + } +} + @Composable private fun CheckboxRow(title: String, checked: Boolean, onCheck: (value: Boolean) -> Unit) { Row( @@ -392,6 +438,6 @@ private val FeedPriority.translationKey: Int get() = when (this) { FeedPriority.MAIN_STREAM -> R.string.freshrss_visibility_option_main FeedPriority.IMPORTANT -> R.string.freshrss_visibility_option_important - FeedPriority.CATEGORY -> R.string.freshrss_visibility_option_category + FeedPriority.CATEGORY -> R.string.freshrss_visibility_option_folder FeedPriority.FEED -> R.string.freshrss_visibility_option_feed } diff --git a/app/src/main/java/com/capyreader/app/ui/articles/feeds/edit/EditFeedViewModel.kt b/app/src/main/java/com/capyreader/app/ui/articles/feeds/edit/EditFeedViewModel.kt index 9a18346cc..638ddf1a1 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/feeds/edit/EditFeedViewModel.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/feeds/edit/EditFeedViewModel.kt @@ -1,13 +1,11 @@ package com.capyreader.app.ui.articles.feeds.edit import androidx.lifecycle.ViewModel -import com.capyreader.app.preferences.AppPreferences import com.jocmp.capy.Account -import com.jocmp.capy.ArticleFilter import com.jocmp.capy.EditFeedFormEntry +import com.jocmp.capy.Feed import com.jocmp.capy.Folder import com.jocmp.capy.common.sortedByTitle -import com.jocmp.capy.preferences.getAndSet import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -15,37 +13,15 @@ import kotlinx.coroutines.withContext class EditFeedViewModel( private val account: Account, - private val appPreferences: AppPreferences ) : ViewModel() { val folders: Flow> = account.folders.map { it.sortedByTitle() } val showMultiselect = account.supportsMultiFolderFeeds suspend fun submit( form: EditFeedFormEntry, - ): Result { + ): Result { return withContext(Dispatchers.IO) { - account - .editFeed(form = form) - .fold( - onSuccess = { feed -> - appPreferences.filter.getAndSet { filter -> - if (filter.isFeedSelected(feed)) { - ArticleFilter.Feeds( - feedID = feed.id, - folderTitle = null, - filter.status - ) - } else { - filter - } - } - - Result.success(Unit) - }, - onFailure = { error -> - Result.failure(error) - } - ) + account.editFeed(form = form) } } } diff --git a/app/src/main/java/com/capyreader/app/ui/articles/list/ArticleActionMenu.kt b/app/src/main/java/com/capyreader/app/ui/articles/list/ArticleActionMenu.kt index 1128b64e2..359b6950d 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/list/ArticleActionMenu.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/list/ArticleActionMenu.kt @@ -2,6 +2,9 @@ package com.capyreader.app.ui.articles.list import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.Label +import androidx.compose.material.icons.outlined.BookmarkBorder +import androidx.compose.material.icons.rounded.ArrowDownward +import androidx.compose.material.icons.rounded.ArrowUpward import androidx.compose.material.icons.rounded.ContentCopy import androidx.compose.material.icons.rounded.Share import androidx.compose.material3.DropdownMenu @@ -10,7 +13,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -36,8 +38,10 @@ fun ArticleActionMenu( article: Article, index: Int, showLabels: Boolean = false, + showSaveForLater: Boolean = false, onMarkAllRead: (range: MarkRead) -> Unit = {}, onOpenLabels: () -> Unit = {}, + onSaveForLater: (url: String) -> Unit = {}, onDismissRequest: () -> Unit = {}, ) { val unreadCount = LocalUnreadCount.current @@ -48,6 +52,9 @@ fun ArticleActionMenu( ) { ToggleStarMenuItem(onDismissRequest, article) ToggleReadMenuItem(onDismissRequest, article) + if (showSaveForLater) { + SaveForLaterMenuItem(onDismissRequest, article, onSaveForLater) + } if (showLabels) { LabelMenuItem(onDismissRequest, onOpenLabels) } @@ -56,7 +63,7 @@ fun ArticleActionMenu( DropdownMenuItem( leadingIcon = { Icon( - painterResource(R.drawable.icon_rounded_arrow_upward), + Icons.Rounded.ArrowUpward, contentDescription = null ) }, @@ -67,7 +74,7 @@ fun ArticleActionMenu( DropdownMenuItem( leadingIcon = { Icon( - painterResource(R.drawable.icon_rounded_arrow_downward), + Icons.Rounded.ArrowDownward, contentDescription = null ) }, @@ -100,6 +107,29 @@ private fun LabelMenuItem( ) } +@Composable +private fun SaveForLaterMenuItem( + onDismissRequest: () -> Unit, + article: Article, + onSaveForLater: (url: String) -> Unit, +) { + val url = article.url?.toString() ?: return + + DropdownMenuItem( + leadingIcon = { + Icon( + Icons.Outlined.BookmarkBorder, + contentDescription = null + ) + }, + text = { Text(stringResource(R.string.article_actions_save_for_later)) }, + onClick = { + onDismissRequest() + onSaveForLater(url) + }, + ) +} + @Composable private fun CopyLinkMenuItem(onDismissRequest: () -> Unit, article: Article) { val url = article.url?.toString() ?: return @@ -168,7 +198,7 @@ private fun ToggleActionMenuItem( DropdownMenuItem( leadingIcon = { Icon( - painterResource(action.icon), + action.icon, contentDescription = null ) }, diff --git a/app/src/main/java/com/capyreader/app/ui/articles/list/ArticleListTopBar.kt b/app/src/main/java/com/capyreader/app/ui/articles/list/ArticleListTopBar.kt index 1902af957..7b58ca1e1 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/list/ArticleListTopBar.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/list/ArticleListTopBar.kt @@ -36,23 +36,21 @@ import com.jocmp.capy.ArticleFilter import com.jocmp.capy.Feed import com.jocmp.capy.Folder import com.jocmp.capy.SavedSearch -import com.jocmp.capy.accounts.Source @OptIn(ExperimentalMaterial3Api::class) @Composable fun ArticleListTopBar( onRequestJumpToTop: () -> Unit, onNavigateToDrawer: () -> Unit, - onRemoveFolder: (folderTitle: String, completion: (result: Result) -> Unit) -> Unit, scrollBehavior: TopAppBarScrollBehavior, onMarkAllRead: () -> Unit, search: ArticleSearch, filter: ArticleFilter, - currentFeed: Feed?, feeds: List, savedSearches: List, folders: List, - source: Source, + hideReadArticles: Boolean = false, + onToggleHideReadArticles: () -> Unit = {}, ) { val enableSearch = search.isActive @@ -140,12 +138,11 @@ fun ArticleListTopBar( actions = { FilterActionMenu( filter = filter, - currentFeed = currentFeed, - onRemoveFolder = onRemoveFolder, onRequestSearch = { search.start() }, onMarkAllRead = { onMarkAllRead() }, hideSearchIcon = enableSearch, - source = source, + hideReadArticles = hideReadArticles, + onToggleHideReadArticles = onToggleHideReadArticles, ) } ) @@ -159,15 +156,12 @@ private fun FeedListTopBarPreview() { ArticleListTopBar( onRequestJumpToTop = { }, onNavigateToDrawer = { }, - onRemoveFolder = { _, _ -> }, scrollBehavior = scrollBehavior, onMarkAllRead = {}, search = ArticleSearch(), filter = ArticleFilter.default(), - currentFeed = null, feeds = listOf(), savedSearches = emptyList(), folders = emptyList(), - source = Source.LOCAL ) } diff --git a/app/src/main/java/com/capyreader/app/ui/articles/list/ArticleRowSwipeState.kt b/app/src/main/java/com/capyreader/app/ui/articles/list/ArticleRowSwipeState.kt index 78c612ca5..82e291a8d 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/list/ArticleRowSwipeState.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/list/ArticleRowSwipeState.kt @@ -2,12 +2,13 @@ package com.capyreader.app.ui.articles.list import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.OpenInNew import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.core.net.toUri @@ -72,7 +73,7 @@ private fun swipeActions(article: Article, option: RowSwipeOption): List Unit, ) @@ -22,14 +20,14 @@ data class ArticleAction( fun readAction(article: Article, actions: ArticleActions) = if (article.read) { ArticleAction( - R.drawable.icon_circle_filled, + Icons.Outlined.FiberManualRecord, R.string.article_view_mark_as_unread, ) { actions.markUnread(article.id) } } else { ArticleAction( - R.drawable.icon_circle_outline, + Icons.Rounded.FiberManualRecord, R.string.article_view_mark_as_read, ) { actions.markRead(article.id) @@ -39,14 +37,14 @@ fun readAction(article: Article, actions: ArticleActions) = fun starAction(article: Article, actions: ArticleActions) = if (article.starred) { ArticleAction( - R.drawable.icon_star_outline, + Icons.Rounded.StarOutline, R.string.article_view_unstar, ) { actions.unstar(article.id) } } else { ArticleAction( - R.drawable.icon_star_filled, + Icons.Rounded.Star, R.string.article_view_star, ) { actions.star(article.id) diff --git a/app/src/main/java/com/capyreader/app/ui/settings/LocalSnackbarHost.kt b/app/src/main/java/com/capyreader/app/ui/components/LocalSnackbarHost.kt similarity index 93% rename from app/src/main/java/com/capyreader/app/ui/settings/LocalSnackbarHost.kt rename to app/src/main/java/com/capyreader/app/ui/components/LocalSnackbarHost.kt index 06a331116..f946ef86f 100644 --- a/app/src/main/java/com/capyreader/app/ui/settings/LocalSnackbarHost.kt +++ b/app/src/main/java/com/capyreader/app/ui/components/LocalSnackbarHost.kt @@ -1,4 +1,4 @@ -package com.capyreader.app.ui.settings +package com.capyreader.app.ui.components import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable diff --git a/app/src/main/java/com/capyreader/app/ui/components/WebView.kt b/app/src/main/java/com/capyreader/app/ui/components/WebView.kt index 496101d6c..5e4b20c0d 100644 --- a/app/src/main/java/com/capyreader/app/ui/components/WebView.kt +++ b/app/src/main/java/com/capyreader/app/ui/components/WebView.kt @@ -24,6 +24,7 @@ import com.capyreader.app.common.WebViewInterface import com.capyreader.app.common.rememberTalkbackPreference import com.capyreader.app.ui.articles.detail.articleTemplateColors import com.capyreader.app.ui.articles.detail.byline +import com.capyreader.app.ui.articles.displayFeedName import com.jocmp.capy.Article import com.jocmp.capy.articles.ArticleRenderer import com.jocmp.capy.logging.CapyLog @@ -180,7 +181,8 @@ class WebViewState( article, hideImages = !showImages, byline = article.byline(context = webView.context), - colors = colors + colors = colors, + feedName = article.displayFeedName(webView.context), ) webView.loadDataWithBaseURL( diff --git a/app/src/main/java/com/capyreader/app/ui/settings/SettingsPanelScaffold.kt b/app/src/main/java/com/capyreader/app/ui/settings/SettingsPanelScaffold.kt index 22a28023a..8e38de03a 100644 --- a/app/src/main/java/com/capyreader/app/ui/settings/SettingsPanelScaffold.kt +++ b/app/src/main/java/com/capyreader/app/ui/settings/SettingsPanelScaffold.kt @@ -1,5 +1,6 @@ package com.capyreader.app.ui.settings +import com.capyreader.app.ui.components.LocalSnackbarHost import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons diff --git a/app/src/main/java/com/capyreader/app/ui/settings/SettingsView.kt b/app/src/main/java/com/capyreader/app/ui/settings/SettingsView.kt index e04a8a281..c9484f10b 100644 --- a/app/src/main/java/com/capyreader/app/ui/settings/SettingsView.kt +++ b/app/src/main/java/com/capyreader/app/ui/settings/SettingsView.kt @@ -22,6 +22,7 @@ import com.capyreader.app.ui.isCompact import com.capyreader.app.ui.provideLinkOpener import com.capyreader.app.ui.settings.panels.AboutSettingsPanel import com.capyreader.app.ui.settings.panels.AccountSettingsPanel +import com.capyreader.app.ui.settings.panels.ArticleListSettingsPanel import com.capyreader.app.ui.settings.panels.DisplaySettingsPanel import com.capyreader.app.ui.settings.panels.GeneralSettingsPanel import com.capyreader.app.ui.settings.panels.GesturesSettingPanel @@ -105,11 +106,15 @@ fun SettingsView( SettingsPanel.Display -> DisplaySettingsPanel( onNavigateToUnreadBadges = { navigateToPanel(SettingsPanel.UnreadBadges) + }, + onNavigateToArticleList = { + navigateToPanel(SettingsPanel.ArticleList) } ) SettingsPanel.Gestures -> GesturesSettingPanel() SettingsPanel.Account -> AccountSettingsPanel(onRemoveAccount = onRemoveAccount) SettingsPanel.About -> AboutSettingsPanel() + SettingsPanel.ArticleList -> ArticleListSettingsPanel() SettingsPanel.UnreadBadges -> UnreadBadgesSettingsPanel( badgeStyle = viewModel.badgeStyle, updateBadgeStyle = viewModel::updateBadgeStyle, diff --git a/app/src/main/java/com/capyreader/app/ui/settings/panels/ArticleListSettings.kt b/app/src/main/java/com/capyreader/app/ui/settings/panels/ArticleListSettings.kt index 89224ebd0..a10dab77b 100644 --- a/app/src/main/java/com/capyreader/app/ui/settings/panels/ArticleListSettings.kt +++ b/app/src/main/java/com/capyreader/app/ui/settings/panels/ArticleListSettings.kt @@ -1,22 +1,48 @@ package com.capyreader.app.ui.settings.panels +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Slider +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.capyreader.app.R import com.capyreader.app.common.ImagePreview import com.capyreader.app.common.RowItem +import com.capyreader.app.preferences.AppTheme import com.capyreader.app.ui.articles.ArticleListFontScale +import com.capyreader.app.ui.articles.ArticleRowOptions +import com.capyreader.app.ui.articles.FaviconBadge +import com.capyreader.app.ui.articles.StyleProviders +import com.capyreader.app.ui.articles.list.ArticleListItem import com.capyreader.app.ui.components.FormSection import com.capyreader.app.ui.components.LabelStyle import com.capyreader.app.ui.components.TextSwitch import com.capyreader.app.ui.settings.PreferenceSelect +import com.capyreader.app.ui.theme.LocalAppTheme import kotlin.math.roundToInt @Immutable @@ -42,6 +68,24 @@ fun ArticleListSettings( val fontScales = ArticleListFontScale.entries Column { + PreviewArticleRow(options = options) + + FormSection( + title = stringResource(R.string.article_font_scale_label), + labelStyle = LabelStyle.COMPACT, + ) { + RowItem { + Slider( + steps = fontScales.size - 2, + valueRange = 0f..(fontScales.size - 1).toFloat(), + value = options.fontScale.ordinal.toFloat(), + onValueChange = { + options.updateFontScale(fontScales[it.roundToInt()]) + } + ) + } + } + RowItem { TextSwitch( onCheckedChange = options.updateFeedName, @@ -75,26 +119,155 @@ fun ArticleListSettings( stringResource(id = it.translationKey) } ) + } +} - FormSection( - modifier = Modifier.padding(top = 16.dp), - title = stringResource(R.string.article_font_scale_label), - labelStyle = LabelStyle.COMPACT, +@Composable +private fun PreviewArticleRow(options: ArticleListOptions) { + val rowOptions = ArticleRowOptions( + showIcon = options.showFeedIcons, + showSummary = options.showSummary, + showFeedName = options.showFeedName, + imagePreview = options.imagePreview, + fontScale = options.fontScale, + shortenTitles = options.shortenTitles, + dim = false, + ) + val colors = ListItemDefaults.colors() + val overlineColor = colors.overlineContentColor + + StyleProviders(options = rowOptions) { + Column( + modifier = Modifier + .padding(16.dp) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant, + shape = MaterialTheme.shapes.medium, + ) ) { - RowItem { - Slider( - steps = fontScales.size - 2, - valueRange = 0f..(fontScales.size - 1).toFloat(), - value = options.fontScale.ordinal.toFloat(), - onValueChange = { - options.updateFontScale(fontScales[it.roundToInt()]) - } + ArticleListItem( + headlineContent = { + Text( + text = PREVIEW_TITLE, + maxLines = if (options.shortenTitles) 3 else Int.MAX_VALUE, + overflow = TextOverflow.Ellipsis, + fontWeight = FontWeight.Bold, ) - } + }, + overlineContent = { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 2.dp) + ) { + if (options.showFeedName) { + Text( + text = PREVIEW_FEED_NAME, + color = overlineColor, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f) + ) + Spacer(Modifier.width(16.dp)) + } + Text( + text = PREVIEW_TIME, + color = overlineColor, + maxLines = 1, + ) + } + }, + supportingContent = if (options.showSummary || options.imagePreview == ImagePreview.LARGE) { + { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.padding(vertical = 4.dp), + ) { + if (options.showSummary) { + Text( + text = PREVIEW_SUMMARY, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + if (options.imagePreview == ImagePreview.LARGE) { + PreviewImage(imagePreview = options.imagePreview) + } + } + } + } else { + null + }, + leadingContent = if (options.showFeedIcons) { + { FaviconBadge(url = null) } + } else { + null + }, + trailingContent = if (options.imagePreview.showInline()) { + { PreviewImage(imagePreview = options.imagePreview) } + } else { + null + }, + ) } } } +@Composable +private fun PreviewImage(imagePreview: ImagePreview) { + val sizeModifier = when (imagePreview) { + ImagePreview.SMALL -> Modifier.size(56.dp) + ImagePreview.MEDIUM -> Modifier.size(84.dp) + else -> Modifier.fillMaxWidth().aspectRatio(3 / 2f) + } + + val shape = MaterialTheme.shapes.small + + Box( + contentAlignment = Alignment.Center, + modifier = sizeModifier + .monochromeBorder(shape) + .clip(shape) + .background(MaterialTheme.colorScheme.surfaceContainer) + ) { + Icon( + painter = painterResource(R.drawable.icon_empty_list), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), + modifier = Modifier.size( + when (imagePreview) { + ImagePreview.SMALL -> 48.dp + ImagePreview.MEDIUM -> 64.dp + else -> 80.dp + } + ) + ) + } +} + +@Composable +private fun Modifier.monochromeBorder(shape: Shape): Modifier { + val isMonochrome = LocalAppTheme.current.value == AppTheme.MONOCHROME + + return if (isMonochrome) { + border( + width = 1.dp, + color = MaterialTheme.colorScheme.outline, + shape = shape, + ) + } else { + this + } +} + +private const val PREVIEW_TITLE = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua" +private const val PREVIEW_FEED_NAME = "Lorem Ipsum" +private const val PREVIEW_TIME = "3h" +private const val PREVIEW_SUMMARY = "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam." + @Preview @Composable private fun ArticleListSettingsPreview() { diff --git a/app/src/main/java/com/capyreader/app/ui/settings/panels/ArticleListSettingsPanel.kt b/app/src/main/java/com/capyreader/app/ui/settings/panels/ArticleListSettingsPanel.kt new file mode 100644 index 000000000..be43ccf21 --- /dev/null +++ b/app/src/main/java/com/capyreader/app/ui/settings/panels/ArticleListSettingsPanel.kt @@ -0,0 +1,26 @@ +package com.capyreader.app.ui.settings.panels + +import androidx.compose.runtime.Composable +import org.koin.androidx.compose.koinViewModel + +@Composable +fun ArticleListSettingsPanel( + viewModel: DisplaySettingsViewModel = koinViewModel(), +) { + ArticleListSettings( + options = ArticleListOptions( + imagePreview = viewModel.imagePreview, + showSummary = viewModel.showSummary, + fontScale = viewModel.fontScale, + showFeedIcons = viewModel.showFeedIcons, + showFeedName = viewModel.showFeedName, + shortenTitles = viewModel.shortenTitles, + updateImagePreview = viewModel::updateImagePreview, + updateSummary = viewModel::updateSummary, + updateFeedName = viewModel::updateFeedName, + updateFeedIcons = viewModel::updateFeedIcons, + updateFontScale = viewModel::updateFontScale, + updateShortenTitles = viewModel::updateShortenTitles, + ) + ) +} diff --git a/app/src/main/java/com/capyreader/app/ui/settings/panels/DisplaySettingsPanel.kt b/app/src/main/java/com/capyreader/app/ui/settings/panels/DisplaySettingsPanel.kt index 20256ad45..d7060da17 100644 --- a/app/src/main/java/com/capyreader/app/ui/settings/panels/DisplaySettingsPanel.kt +++ b/app/src/main/java/com/capyreader/app/ui/settings/panels/DisplaySettingsPanel.kt @@ -13,8 +13,11 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight import androidx.compose.material3.ButtonGroupDefaults import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Icon import androidx.compose.material3.ListItem import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -27,13 +30,11 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import com.capyreader.app.R -import com.capyreader.app.common.ImagePreview import com.capyreader.app.common.RowItem import com.capyreader.app.preferences.AppPreferences import com.capyreader.app.preferences.AppTheme import com.capyreader.app.preferences.ReaderImageVisibility import com.capyreader.app.preferences.ThemeMode -import com.capyreader.app.ui.articles.ArticleListFontScale import com.capyreader.app.ui.articles.MarkReadPosition import com.capyreader.app.ui.collectChangesWithCurrent import com.capyreader.app.ui.components.FormSection @@ -47,6 +48,7 @@ import org.koin.androidx.compose.koinViewModel fun DisplaySettingsPanel( viewModel: DisplaySettingsViewModel = koinViewModel(), onNavigateToUnreadBadges: () -> Unit = {}, + onNavigateToArticleList: () -> Unit = {}, ) { val pinArticleBars by viewModel.pinArticleBars.collectChangesWithCurrent() val improveTalkback by viewModel.improveTalkback.collectChangesWithCurrent() @@ -70,20 +72,7 @@ fun DisplaySettingsPanel( markReadButtonPosition = markReadButtonPosition, updateMarkReadButtonPosition = viewModel::updateMarkReadButtonPosition, onNavigateToUnreadBadges = onNavigateToUnreadBadges, - articleListOptions = ArticleListOptions( - imagePreview = viewModel.imagePreview, - showSummary = viewModel.showSummary, - fontScale = viewModel.fontScale, - showFeedIcons = viewModel.showFeedIcons, - showFeedName = viewModel.showFeedName, - shortenTitles = viewModel.shortenTitles, - updateImagePreview = viewModel::updateImagePreview, - updateSummary = viewModel::updateSummary, - updateFeedName = viewModel::updateFeedName, - updateFeedIcons = viewModel::updateFeedIcons, - updateFontScale = viewModel::updateFontScale, - updateShortenTitles = viewModel::updateShortenTitles, - ) + onNavigateToArticleList = onNavigateToArticleList, ) } @@ -105,7 +94,7 @@ fun DisplaySettingsPanelView( updateImageVisibility: (option: ReaderImageVisibility) -> Unit, updateMarkReadButtonPosition: (position: MarkReadPosition) -> Unit, onNavigateToUnreadBadges: () -> Unit = {}, - articleListOptions: ArticleListOptions, + onNavigateToArticleList: () -> Unit = {}, ) { Column( verticalArrangement = Arrangement.spacedBy(16.dp), @@ -145,15 +134,17 @@ fun DisplaySettingsPanelView( ) } } - Box(Modifier.clickable { onNavigateToUnreadBadges() }) { - ListItem( - headlineContent = { - Text(stringResource(R.string.settings_panel_unread_counts_title)) - } - ) - } + SettingsDisclosureRow( + title = stringResource(R.string.settings_panel_unread_counts_title), + onClick = onNavigateToUnreadBadges, + ) } + SettingsDisclosureRow( + title = stringResource(R.string.settings_article_list_title), + onClick = onNavigateToArticleList, + ) + FormSection( title = stringResource(R.string.settings_reader_title) ) { @@ -176,14 +167,6 @@ fun DisplaySettingsPanelView( } } - FormSection( - title = stringResource(R.string.settings_article_list_title) - ) { - ArticleListSettings( - options = articleListOptions - ) - } - FormSection(title = stringResource(R.string.settings_display_miscellaneous_title)) { PreferenceSelect( selected = markReadButtonPosition, @@ -200,6 +183,24 @@ fun DisplaySettingsPanelView( } } +@Composable +private fun SettingsDisclosureRow( + title: String, + onClick: () -> Unit, +) { + Box(Modifier.clickable { onClick() }) { + ListItem( + headlineContent = { Text(title) }, + trailingContent = { + Icon( + imageVector = Icons.AutoMirrored.Rounded.KeyboardArrowRight, + contentDescription = null, + ) + } + ) + } +} + @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable private fun ThemeModeButtons( @@ -240,20 +241,6 @@ private fun DisplaySettingsPanelViewPreview() { pureBlackDarkMode = false, updatePureBlackDarkMode = {}, appPreferences = null, - articleListOptions = ArticleListOptions( - imagePreview = ImagePreview.default, - showSummary = true, - fontScale = ArticleListFontScale.MEDIUM, - showFeedIcons = true, - showFeedName = false, - shortenTitles = true, - updateImagePreview = {}, - updateSummary = {}, - updateFeedName = {}, - updateFeedIcons = {}, - updateFontScale = {}, - updateShortenTitles = {}, - ), updatePinArticleBars = {}, pinArticleBars = false, updateImageVisibility = {}, diff --git a/app/src/main/java/com/capyreader/app/ui/settings/panels/GeneralSettingsPanel.kt b/app/src/main/java/com/capyreader/app/ui/settings/panels/GeneralSettingsPanel.kt index 03a04fd75..112b4c19b 100644 --- a/app/src/main/java/com/capyreader/app/ui/settings/panels/GeneralSettingsPanel.kt +++ b/app/src/main/java/com/capyreader/app/ui/settings/panels/GeneralSettingsPanel.kt @@ -42,13 +42,14 @@ import com.capyreader.app.R import com.capyreader.app.common.RowItem import com.capyreader.app.notifications.Notifications import com.capyreader.app.preferences.AfterReadAllBehavior +import com.capyreader.app.preferences.HomePage import com.capyreader.app.refresher.RefreshInterval import com.capyreader.app.ui.CrashReporting import com.capyreader.app.ui.components.FormSection import com.capyreader.app.ui.components.TextSwitch import com.capyreader.app.ui.fixtures.PreviewKoinApplication import com.capyreader.app.ui.settings.CrashReportingCheckbox -import com.capyreader.app.ui.settings.LocalSnackbarHost +import com.capyreader.app.ui.components.LocalSnackbarHost import com.capyreader.app.ui.settings.PreferenceSelect import com.capyreader.app.ui.settings.filters.FilterKeywords import com.capyreader.app.ui.settings.filters.FiltersItem @@ -66,6 +67,7 @@ fun GeneralSettingsPanel( viewModel: GeneralSettingsViewModel = koinViewModel(), onNavigateToNotifications: () -> Unit, ) { + val hasReadLaterFeed by viewModel.hasReadLaterFeed.collectAsStateWithLifecycle(initialValue = false) val keywords by viewModel.filterKeywords.collectAsStateWithLifecycle() val filterKeywords = FilterKeywords( @@ -97,8 +99,9 @@ fun GeneralSettingsPanel( updateAfterReadAll = viewModel::updateAfterReadAll, updateStickyFullContent = viewModel::updateStickyFullContent, enableStickyFullContent = viewModel.enableStickyFullContent, - showTodayFilter = viewModel.showTodayFilter, - updateShowTodayFilter = viewModel::updateShowTodayFilter, + homePage = viewModel.homePage, + updateHomePage = viewModel::updateHomePage, + hasReadLaterFeed = hasReadLaterFeed, ) } } @@ -124,8 +127,9 @@ fun GeneralSettingsPanelView( updateAfterReadAll: (behavior: AfterReadAllBehavior) -> Unit, confirmMarkAllRead: Boolean, markReadOnScroll: Boolean, - showTodayFilter: Boolean, - updateShowTodayFilter: (show: Boolean) -> Unit, + homePage: HomePage = HomePage.default, + updateHomePage: (HomePage) -> Unit = {}, + hasReadLaterFeed: Boolean = false, ) { val (isClearArticlesDialogOpen, setClearArticlesDialogOpen) = remember { mutableStateOf(false) } @@ -147,15 +151,11 @@ fun GeneralSettingsPanelView( updateSortOrder ) - FormSection(title = stringResource(R.string.settings_section_categories)) { - RowItem { - TextSwitch( - checked = showTodayFilter, - onCheckedChange = updateShowTodayFilter, - title = stringResource(R.string.settings_option_show_today_filter) - ) - } - } + HomePageSelect( + selected = homePage, + update = updateHomePage, + showReadLater = hasReadLaterFeed, + ) FormSection(title = stringResource(R.string.settings_section_refresh)) { Column { @@ -371,8 +371,6 @@ private fun GeneralSettingsPanelPreview() { enableStickyFullContent = true, afterReadAll = AfterReadAllBehavior.NOTHING, updateAfterReadAll = {}, - showTodayFilter = true, - updateShowTodayFilter = {}, ) } } diff --git a/app/src/main/java/com/capyreader/app/ui/settings/panels/GeneralSettingsViewModel.kt b/app/src/main/java/com/capyreader/app/ui/settings/panels/GeneralSettingsViewModel.kt index edc89a455..b8cc5a696 100644 --- a/app/src/main/java/com/capyreader/app/ui/settings/panels/GeneralSettingsViewModel.kt +++ b/app/src/main/java/com/capyreader/app/ui/settings/panels/GeneralSettingsViewModel.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.capyreader.app.preferences.AfterReadAllBehavior import com.capyreader.app.preferences.AppPreferences +import com.capyreader.app.preferences.HomePage import com.capyreader.app.refresher.RefreshInterval import com.capyreader.app.refresher.RefreshScheduler import com.jocmp.capy.Account @@ -14,6 +15,7 @@ import com.jocmp.capy.accounts.AutoDelete import com.jocmp.capy.articles.SortOrder import com.jocmp.capy.preferences.getAndSet import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch class GeneralSettingsViewModel( @@ -47,9 +49,11 @@ class GeneralSettingsViewModel( var enableStickyFullContent by mutableStateOf(appPreferences.enableStickyFullContent.get()) private set - var showTodayFilter by mutableStateOf(appPreferences.showTodayFilter.get()) + var homePage by mutableStateOf(appPreferences.homePage.get()) private set + val hasReadLaterFeed = account.feeds.map { feeds -> feeds.any { it.isReadLater } } + val filterKeywords = account .preferences .filterKeywords @@ -127,9 +131,9 @@ class GeneralSettingsViewModel( } } - fun updateShowTodayFilter(show: Boolean) { - appPreferences.showTodayFilter.set(show) + fun updateHomePage(homePage: HomePage) { + appPreferences.homePage.set(homePage) - showTodayFilter = show + this.homePage = homePage } } diff --git a/app/src/main/java/com/capyreader/app/ui/settings/panels/HomePageSelect.kt b/app/src/main/java/com/capyreader/app/ui/settings/panels/HomePageSelect.kt new file mode 100644 index 000000000..917987083 --- /dev/null +++ b/app/src/main/java/com/capyreader/app/ui/settings/panels/HomePageSelect.kt @@ -0,0 +1,44 @@ +package com.capyreader.app.ui.settings.panels + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.res.stringResource +import com.capyreader.app.R +import com.capyreader.app.preferences.HomePage +import com.capyreader.app.ui.settings.PreferenceSelect + +@Composable +fun HomePageSelect( + selected: HomePage, + update: (HomePage) -> Unit, + showReadLater: Boolean = false, +) { + val options = remember(showReadLater) { + buildList { + add(HomePage.Today) + add(HomePage.Unread) + add(HomePage.Starred) + if (showReadLater) { + add(HomePage.ReadLater) + } + } + } + + PreferenceSelect( + selected = selected, + update = update, + options = options, + optionText = { homePageLabel(it) }, + label = R.string.settings_home_page, + ) +} + +@Composable +private fun homePageLabel(homePage: HomePage): String { + return when (homePage) { + is HomePage.Today -> stringResource(R.string.filter_today) + is HomePage.Unread -> stringResource(R.string.filter_unread) + is HomePage.Starred -> stringResource(R.string.filter_starred) + is HomePage.ReadLater -> stringResource(R.string.filter_read_later) + } +} diff --git a/app/src/main/java/com/capyreader/app/ui/settings/panels/SettingsPanel.kt b/app/src/main/java/com/capyreader/app/ui/settings/panels/SettingsPanel.kt index 8bc510e5d..7196724d4 100644 --- a/app/src/main/java/com/capyreader/app/ui/settings/panels/SettingsPanel.kt +++ b/app/src/main/java/com/capyreader/app/ui/settings/panels/SettingsPanel.kt @@ -54,6 +54,12 @@ sealed class SettingsPanel(@StringRes val title: Int) { override fun icon() = Icons.Rounded.Visibility } + @Parcelize + data object ArticleList : SettingsPanel(title = R.string.settings_article_list_title), + Parcelable { + override fun icon() = Icons.Rounded.Visibility + } + fun isNested() = !items.contains(this) companion object { diff --git a/app/src/main/res/drawable/icon_circle_outline.xml b/app/src/main/res/drawable/icon_circle_outline.xml deleted file mode 100644 index ca2728451..000000000 --- a/app/src/main/res/drawable/icon_circle_outline.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/icon_open_in_new.xml b/app/src/main/res/drawable/icon_open_in_new.xml deleted file mode 100644 index 7bdb080aa..000000000 --- a/app/src/main/res/drawable/icon_open_in_new.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/icon_rounded_arrow_downward.xml b/app/src/main/res/drawable/icon_rounded_arrow_downward.xml deleted file mode 100644 index 3021f83d3..000000000 --- a/app/src/main/res/drawable/icon_rounded_arrow_downward.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/icon_rounded_arrow_upward.xml b/app/src/main/res/drawable/icon_rounded_arrow_upward.xml deleted file mode 100644 index f3bae229d..000000000 --- a/app/src/main/res/drawable/icon_rounded_arrow_upward.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/icon_star_filled.xml b/app/src/main/res/drawable/icon_star_filled.xml deleted file mode 100644 index bedd26db4..000000000 --- a/app/src/main/res/drawable/icon_star_filled.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/icon_star_outline.xml b/app/src/main/res/drawable/icon_star_outline.xml deleted file mode 100644 index b3fff6d86..000000000 --- a/app/src/main/res/drawable/icon_star_outline.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 872191250..2bc9b49aa 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -29,7 +29,7 @@ يدوياً كل %1$d دقيقة حدِّث الموجز - المجلدات + المجلدات الموجز أضف موجز تعذر العثور على الموجز @@ -87,7 +87,7 @@ فاتح داكن الوضع الإفتراضي للنظام - المجلدات + المجلدات إستخدم المتصفح داخل التطبيق توقف لا شيء @@ -182,7 +182,7 @@ يحفظ سجلات الأعطال في ملف لمشاركتها مع المطور القارئ إنتقل إلى المقال التالي - المجلدات + المجلدات تمكين الإشعارات الإعدادات إلغاء تحديد الكل @@ -204,7 +204,6 @@ قم بإنشاء حساب من هنا. ليس لديك نسخة FreshRSS ؟ اكتشف المزيد. - إضافة أو تحديد المجلد ضع علامة كمقروء تلقائيًا خلال التمرير وضع علامة كمقروء على الكل المتصفح @@ -260,11 +259,11 @@ تعديل المجلد نسخ الرابط مشاركة الرابط - خطأ في حذف المجلد - الأسم - هل أنت متأكد أنك تريد حذف مجلد \"%1$s\"؟ - تم تحديث المجلد - حذف + خطأ في حذف المجلد + الأسم + هل أنت متأكد أنك تريد حذف مجلد \"%1$s\"؟ + تم تحديث المجلد + حذف العناوين الرئيسية أحدث العناوين عرض المزيد من المقالات @@ -295,10 +294,10 @@ الفئات اليوم كلمة مرور API - الرؤية - التيار الرئيسي + الرؤية + التيار الرئيسي مهم - فئة + فئة موجز الوضع شغِّل diff --git a/app/src/main/res/values-b+sr+Latn/strings.xml b/app/src/main/res/values-b+sr+Latn/strings.xml index 7006ea8b4..0d8788fd5 100644 --- a/app/src/main/res/values-b+sr+Latn/strings.xml +++ b/app/src/main/res/values-b+sr+Latn/strings.xml @@ -40,7 +40,7 @@ Svako %d sati Osveži izvore - Tagovi + Tagovi Izvori Dodaj Izvor Nije moguće pronaći Izvor @@ -93,7 +93,7 @@ Svetla Tamna Podrazumevana tema sistema - Tagovi + Tagovi Koristite pregledač u aplikaciji Odustani Bez Slike @@ -165,7 +165,7 @@ Prvo Najstariji Podrazumevano Delite evidenciju o pada aplikacije - Oznake + Oznake Omogući obaveštenja Prvo mora biti omogućeno periodično osvežavanje Izaberite Ništa @@ -198,7 +198,6 @@ Nemate FreshRSS instancu? Saznajte više. Server - Tag Server Označi kao pročitano na skrolovanju Označi sve kao pročitano diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index cedc6ae25..309507df0 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -1,6 +1,6 @@ - Етикети + Етикети Capy Reader Добавяне на профил Изнасяне на абонаменти @@ -8,7 +8,7 @@ Внасяне на абонаменти %1$d от %2$d Feedbin Локален - Етикети + Етикети Възникна проблем при свързването с вашия профил. Моля, влезте отново. Скрий паролата Настройки @@ -182,7 +182,7 @@ Няма избрано Активирай известията Разрешението за известие е деактивирано - Етикети + Етикети Известия Първо трябва да се активира периодично обновяване Настройки @@ -201,7 +201,6 @@ https://example.com/ Самостоятелно хостван Google Reader API Създайте такъв тук. - Добавяне или избиране на етикет Нямате FreshRSS инстанция? Докосни за превъртане (за E-ink) Докоснете долните ъгли, за да преминете към съдържанието на статията @@ -293,10 +292,10 @@ Категории Днес Парола за API - Видимост - Основен поток + Видимост + Основен поток Важно - Категория + Категория Емисия Режим Експериментално diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 26ec61f56..5dd2124fc 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -26,7 +26,7 @@ Každé %d hodiny Každých %d hodin - Štítky + Štítky Právě teď %1$dmin %1$dhod @@ -56,7 +56,7 @@ Světlý Tmavý Stejný jako systém - štítky + štítky Otevírat odkazy uvnitř aplikace Bez náhledu Malá @@ -180,7 +180,7 @@ Aktualizace zdroje Otevřít externě Upozornění - Štítky + Štítky Nastavení Odesílání oznámení není povoleno Reader @@ -190,7 +190,6 @@ Vytvořte si ho zde. Nemáte instanci FreshRSS? Zjistit více. - Vytvořit nebo vybrat štítek Nevybrat nic Vybrat vše Potvrdit označení všeho jako přečtené @@ -249,7 +248,7 @@ Novinový papír Monochromatický Čistě černý tmavý režim - Štítek se nepodařilo odstranit + Štítek se nepodařilo odstranit Klientský certifikát (nepovinné) Odebrat klientský certifikát Nemáte nainstalovaný Miniflux? @@ -290,8 +289,8 @@ Kategorie Dnešní API heslo - Viditelnost - Hlavní kanál + Viditelnost + Hlavní kanál Důležité - Kategorie + Kategorie diff --git a/app/src/main/res/values-cy/strings.xml b/app/src/main/res/values-cy/strings.xml index b116267eb..08e96ab89 100644 --- a/app/src/main/res/values-cy/strings.xml +++ b/app/src/main/res/values-cy/strings.xml @@ -34,7 +34,7 @@ Rhoi seren Cael gwared ar y seren Adnewyddu ffrydiau - Tagiau + Tagiau Ffrydiau Ychwanegu ffrwd Methu dod o hyd y ffrwd @@ -82,7 +82,7 @@ Thema Golau Rhagosodiad y system - Tagiau + Tagiau Defnyddio\'r porwr mewn yr ap Stopio Dim @@ -126,8 +126,7 @@ Hynaf yn gyntaf Rhagosodiad Rhannu logiau\'r chwalu - Tagiau - Ychwanegu neu dewis tag + Tagiau Galluogi hysbysiadau Gosodiadau Agor yr erthygl mewn porwr @@ -139,8 +138,6 @@ Ydych chi\'n siŵr eich bod chi am ddad-danysgrifio o \"%1$s\"? Cadw Diddymu - Dileu\'r tag - Ailenwi Agor y gosodiadau Nawr Mewngofnodi @@ -251,8 +248,8 @@ Dileu erthyglau gydag allweddeiriau yn y teitl neu\'r cynnwys Rhannu\'r ddolen Chwiliadau - Bu gwall wrth ddileu tag - Enw + Bu gwall wrth ddileu tag + Enw Wedi\'i analluogi Mynd i\'r ffrwd nesaf Llusgo i\'r chwith neu\'r dde i lywio erthyglau diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index fe326bd88..d13a734b9 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -42,14 +42,14 @@ Hver %d. time Opdater feeds - Etiketter + Etiketter Søgninger Mine etiketter Feeds Kunne ikke finde feed Kunne ikke gemme feed Kunne ikke finde feed. Tjek din forbindelse. - Fejl ved sletning af etikette + Fejl ved sletning af etikette Lige nu %1$dm %1$dt @@ -132,7 +132,7 @@ Lys Mørk System standard - Etiketter + Etiketter Brug browser i applikationen Stop Ingen @@ -222,8 +222,7 @@ Del Del fejlrapport Gem fejlrapport som en fil til deling med udvikleren - Etiketter - Tilføj eller vælg etikette + Etiketter Slå notifikationer til Indstillinger Notifikationstilladelse ej givet @@ -286,10 +285,10 @@ Åbn artikler i browser Valgt API adgangskode - Synlighed - Hovedstrøm + Synlighed + Hovedstrøm Vigtigt - Kategori + Kategori Feed Afspil Pause diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index b765beec6..ab3c002b4 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -29,7 +29,7 @@ Nur manuell Alle %1$d Minuten Feeds aktualisieren - Bezeichnungen + Bezeichnungen Feeds Feed hinzufügen Feed konnte nicht gefunden werden @@ -74,7 +74,7 @@ Dynamisch Zeitungspapier Monochrom - Bezeichnungen + Bezeichnungen In-App-Browser benutzen Stop Keines @@ -189,12 +189,11 @@ Aktion bei Zurück Geste Standard Speichert Absturzberichte in Datei, um sie mit dem Entwickler zu teilen - Schlagwörter + Schlagwörter Nichts ausgewählt Alles auswählen Navigationsübersicht öffnen Auf deinem Gerät - Bezeichnung hinzufügen oder auswählen Server Server Reader @@ -248,14 +247,12 @@ Bild speichern fehlgeschlagen Bild teilen fehlgeschlagen Alle aktualisieren - Fehler beim Löschen - Löschen - Bist Du sicher, dass Du \"%1$s\" löschen möchtest? - Name - Bezeichnung aktualisiert - Fehler bei der Aktualisierung der Bezeichnung - Bezeichnung bearbeiten - Löschen + Fehler beim Löschen + Bist Du sicher, dass Du \"%1$s\" löschen möchtest? + Name + Bezeichnung aktualisiert + Fehler bei der Aktualisierung der Bezeichnung + Löschen Feeds Artikel entfernen welche dieses Schlagwort im Titel oder Inhalt enthalten Schlagwort entfernen @@ -291,7 +288,7 @@ Kategorien Heute API-Passwort - Sichtbarkeit + Sichtbarkeit Kategorien Modus Leer hier @@ -300,7 +297,7 @@ Passwort stattdessen verwenden Erweiterte Optionen Erweiterte Optionen ausblenden - Kategorie + Kategorie Abspielen Pause Player schließen @@ -329,7 +326,7 @@ Löschen Bist du sicher, dass du diese Seite löschen willst? Löschen - Main Stream + Main Stream Speichern Gespeichert Speichern fehlgeschlagen diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 344477111..069bb62b0 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -30,7 +30,7 @@ Κάθε %d ώρες Ανανέωση Ροών - Ετικέτες + Ετικέτες Αναζητήσεις Ροές Προσθήκη Ροής @@ -57,7 +57,7 @@ Προστέθηκε ροή Όλα %1$dλ - Σφάλμα διαγραφής ετικέτας + Σφάλμα διαγραφής ετικέτας Feedbin Απόκρυψη κωδικού Σύνδεση @@ -127,7 +127,7 @@ Φωτεινό Σκοτεινό Προεπιλογή συστήματος - Ετικέτες + Ετικέτες Χρήση περιηγητή εντός εφαρμογής Διακοπή Καμία @@ -207,8 +207,7 @@ Κοινοποίηση Κοινοποίηση καταγραφής σφαλμάτων Αποθηκεύει αρχεία καταγραφής σε ένα αρχείο για να τα κοινοποιήσεις με τον προγραμματιστή - Ετικέτες - Προσθήκη ή Επιλογή Ετικέτας + Ετικέτες Ενεργοποίηση ειδοποιήσεων Ρυθμίσεις Η άδεια ειδοποιήσεων είναι ανενεργή @@ -257,11 +256,10 @@ Πρόσθεσε ένα λογαριασμό πρώτα Βελτίωση TalkBack Απενεργοποιεί κάποιες ρυθμίσεις για βελτίωση του κειμένου σε ομιλία TalkBack - Διαγραφή - \'Ονομα - Ετικέτα ενημερώθηκε - Σφάλμα ενημέρωσης ετικέτας - Επεξεργασία Ετικέτας + Διαγραφή + \'Ονομα + Ετικέτα ενημερώθηκε + Σφάλμα ενημέρωσης ετικέτας Αντιγραφή συνδέσμου Κοινοποίηση συνδέσμου Επικεφαλίδες @@ -286,10 +284,10 @@ Κατηγορίες Σήμερα Κωδικός API - Ορατότητα - Κύρια Ροή + Ορατότητα + Κύρια Ροή Σημαντικό - Κατηγορία + Κατηγορία Ροή Κατηγορίες Λειτουργία diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index ec635bd34..b630c93a4 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -40,7 +40,7 @@ Cada %d horas Recargar los feeds - Etiquetas + Etiquetas Feeds Añadir un feed No se ha encontrado el feed @@ -96,7 +96,7 @@ Dinámico Papel prensa Monocromo - Etiquetas + Etiquetas Utilizar el navegador de la aplicación Detener Ninguna @@ -182,7 +182,7 @@ Guardar los registros de errores en un archivo para compartirlo con el desarrollador Compartir Compartir registros de errores - Etiquetas + Etiquetas Activar las notificaciones Notificaciones desactivadas Actualizaciones de feeds @@ -203,7 +203,6 @@ Crea una aquí. ¿No tienes una instancia de FreshRSS ? Descubra más. - Etiqueta Lector Tocar las esquinas inferiores para navegar por el contenido del artículo Presione para desplazarse por la tinta electrónica @@ -258,12 +257,11 @@ Editar Etiqueta Copiar enlace Compartir enlace - ¿Eliminar la etiqueta \"%1$s\"? - Eliminar - Nombre - Eliminar - Etiqueta actualizada - Error al actualizar etiqueta + ¿Eliminar la etiqueta \"%1$s\"? + Nombre + Eliminar + Etiqueta actualizada + Error al actualizar etiqueta Titulares Últimos Titulares Ver mas artículos @@ -292,9 +290,9 @@ Abrir artículos en el navegador Seleccionado Contraseña de API - Visibilidad + Visibilidad Importante - Categoría + Categoría Modo Reproducir Pausar @@ -304,7 +302,7 @@ Categorías Nada por aquí Contenido - Corriente principal + Corriente principal Token de API Usar Contraseña en su lugar Opciones avanzadas diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index a26148233..6f2c4f85b 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -30,7 +30,7 @@ Vaid käsitsi Iga %1$d minuti järel Uuenda uudisvooge - Sildid + Sildid Uudisvood Lisa uudisvoog Uudisvoogu ei õnnestunud leida @@ -67,7 +67,7 @@ Impordime tellimusi… Impordime tellimusi: %1$d/%2$d Imporditud - Sildid + Sildid Kasuta rakenduse-sisest veebibrauserit Pole kasutusel Väikesed pildid @@ -176,7 +176,7 @@ Jagamiseks arendajaga salvesta rakenduse kokkujooksmise andmed tekstifaili Vaikimisi määratud Uudisvoo uuendused - Sildid + Sildid Võta teavitused kasutusele Puuduvad õigused teavituse saatmiseks Esmalt võta kasutusele korraline andmete uuendus @@ -198,7 +198,6 @@ https://example.com/ Uudistelugeja Oma serveris asuv Google Readeri API - Lisa või vali silt Kas sul pole Feedbin kontot? Sul pole FreshRSSi serverit? Kerimiseks klõpsi @@ -292,10 +291,10 @@ Kategooriad Täna API salasõna - Nähtavus - Põhiline uudisvoog + Nähtavus + Põhiline uudisvoog Oluline - Kategooria + Kategooria Uudisvoog Režiim Katseline diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index ca43bc868..3ff037de4 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -19,7 +19,7 @@ Voulez-vous vraiment supprimer votre compte ? Cela ne peut être annulé. Confirmer Nom enregistré - Étiquettes + Étiquettes Impossible de trouver un flux. Vérifiez votre connexion. Impossible de trouver un flux Impossible d’enregistrer le flux @@ -151,7 +151,7 @@ Ajouter un compte Exporter les abonnements Importation échouée - Étiquettes + Étiquettes Aucun Grand Version @@ -188,7 +188,7 @@ Notifications Activer les notifications Mises à jour des flux - Étiquettes + Étiquettes Tout sélectionner Ne rien sélectionner Action de navigation retour @@ -202,7 +202,6 @@ API Google Reader auto-hébergée Créez-en un ici. En savoir plus. - Ajouter ou sélectionner une étiquette Appuyez sur les coins inférieurs pour parcourir le contenu de l\'article Vous n’avez pas de compte Feedbin ? Vous n’avez pas d’instance FreshRSS ? @@ -292,10 +291,10 @@ Catégories Aujourd\'hui Mot de passe API - Visibilité - Flux Principal + Visibilité + Flux Principal Important - Catégorie + Catégorie Flux Mode Lecture diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 9466c605b..7ab47f774 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -104,7 +104,7 @@ A importación fallou Decorado Seguir ao sistema - Etiquetas + Etiquetas Pequena Grande Vista previa de imaxes @@ -168,8 +168,7 @@ Compartir Compartir informe de fallos Garda un informe do fallo nun ficheiro para compartilo coas desenvolvedoras - Etiquetas - Engadir ou seleccionar etiqueta + Etiquetas Activar notificacións Primeiro debes activar a actualización periódica Sen selección @@ -192,7 +191,7 @@ Importar Desbotar Retirar estrela - Etiquetas + Etiquetas Non se atopou a canle Mostrar contrasinal Tes certeza de querer eliminar a túa conta? Non haberá volta atrás. @@ -286,10 +285,10 @@ Categorías Hoxe Contrasinal da API - Visibilidade - Cronoloxía principal + Visibilidade + Cronoloxía principal Importante - Categoría + Categoría Cronoloxía Modo Experimental diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index f2f60d5c6..a6d89f4a4 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -37,13 +37,13 @@ Csak manuálisan Minden %1$d percben Hírfolyamok frissítése - Címkék + Címkék Keresések Hírfolyamok Hírfolyam hozzáadása Nem található hírfolyam Nem sikerült menteni a hírfolyamot - Hiba a címke törlésekor + Hiba a címke törlésekor Épp most %1$d p %1$d ó @@ -121,7 +121,7 @@ Világos Sötét Rendszer alapértelmezett - Címkék + Címkék Alkalmazáson belüli böngésző használata Leállítás Nincs @@ -208,8 +208,7 @@ Megosztás Összeomlási naplók megosztása Összeomlási naplókat ment fájlba, hogy megoszthassa a fejlesztővel - Címkék - Címke hozzáadása vagy kiválasztása + Címkék Értesítések engedélyezése Beállítások Értesítési engedély le van tiltva @@ -275,10 +274,10 @@ Cikkek megnyitása böngészőben Kiválasztott API jelszó - Láthatóság - Fő hírfolyam + Láthatóság + Fő hírfolyam Fontos - Kategória + Kategória Hírfolyam Biztosan törölni szeretné a fiókját? Ez a művelet nem vonható vissza. diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 3773d6cad..0a05075a3 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -34,7 +34,7 @@ Hanya secara manual Setiap %1$d menit Segarkan Umpan - Label + Label Umpan Umpan tidak ditemukan Umpan tidak dapat disimpan @@ -75,7 +75,7 @@ Ekspor Tema Terang - Label + Label Akun Tentang Pengaturan @@ -177,7 +177,7 @@ Daftar Artikel Tindakan Navigasi Kembali Simpan catatan kerusakan ke file untuk dibagikan dengan pengembang - Label + Label Izin notifikasi dinonaktifkan Pengaturan Nama pengguna @@ -207,7 +207,6 @@ Batalkan pencarian Bawaan Buka Laci Navigasi - Tambah atau Pilih Label Hidupkan notifikasi Penyegaran berkala harus diaktifkan terlebih dahulu Pilih Semuanya @@ -243,7 +242,7 @@ Kesalahan saat perbarui label Nama Bagikan tautan - Kesalahan saat menghapus label + Kesalahan saat menghapus label Usap ke Atas Ke umpan selanjutnya Simpan @@ -306,10 +305,10 @@ Gunakan Kata Sandi sebagai gantinya Opsi lanjutan Sembunyikan opsi lanjutan - Visibilitas - Aliran Utama + Visibilitas + Aliran Utama Penting - Kategori + Kategori Umpan Edit Kategori Putar diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index b257ea931..2097f6413 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -19,7 +19,7 @@ Annulla Conferma Nome salvato - Tag + Tag Feed Vuoi davvero eliminare il tuo account? Non è possibile annullare questa operazione. Impossibile trovare il feed. Controllare la connessione. @@ -59,7 +59,7 @@ Chiaro Scuro Predefinito dal sistema - Tag + Tag Usa il browser in-app Stop Nessuna @@ -186,7 +186,7 @@ Aggiornamento dei feed Notifiche Le notifiche sono disabilitate - Tag + Tag Impostazioni Apri l\'articolo nel browser Apri esternamente @@ -202,7 +202,6 @@ Non hai un account Feedbin? Creane uno qui. Non hai un\'istanza FreshRSS? - Aggiungi o Seleziona Tag feedbin.com Per saperne di più. Tocca gli angoli inferiori per navigare nel contenuto dell\'articolo @@ -295,10 +294,10 @@ Categorie Oggi Password API - Visibilità - Flusso principale + Visibilità + Flusso principale Importante - Categoria + Categoria Feed Modalità Riproduci diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 12d0a606c..7eb60e2b6 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -41,12 +41,12 @@ כל %d שעות מרענן את הפיד - תגיות + תגיות פידים לא ניתן למצוא פיד לא ניתן לשמור פיד העמוד נשמר - שגיאה במחיקת התגית + שגיאה במחיקת התגית %1$dש שיתוף מאמר סמן הכל כנקרא @@ -125,7 +125,7 @@ בהיר חשוך ברירת מחדל מערכת - תגיות + תגיות פתח בדפדפן המובנה עצור כלום @@ -214,8 +214,7 @@ שתף שתף לוגים שמור לוגים לקובץ לשיתוף עם המפתח - תגיות - הוסף או בחר תגית + תגיות אפשר התראות אפשרויות הרשאת התראות מכובה @@ -313,10 +312,10 @@ השתמש בסיסמה במקום אפשרויות מתקדמות הסתר אפשרויות מתקדמות - נראות - זרם מרכזי + נראות + זרם מרכזי חשוב - קטגוריה + קטגוריה פיד ערוך קטגוריה נגן diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index d30705129..24e33efa4 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -18,8 +18,6 @@ 購読解除 保存 キャンセル - タグを削除 - 名前を変更 購読解除 設定を開く ログアウト @@ -41,14 +39,14 @@ %d 時間ごと フィードの更新 - タグ + タグ 検索 フィード フィードを追加 フィードが見つかりません フィードを保存できませんでした フィードが見つかりません。接続を確認してください。 - タグの削除エラー + タグの削除エラー たった今 %1$d分 %1$d時間 @@ -124,7 +122,7 @@ ライト ダーク システムのデフォルト - タグ + タグ アプリ内ブラウザを使用 停止 なし @@ -202,8 +200,7 @@ 共有 クラッシュログを共有 開発者と共有するため、ファイルにクラッシュログを保存します - タグ - タグを追加または選択 + タグ 通知を有効化 設定 通知の権限が無効になっています diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index 15f4dc680..6acf8a5d1 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -13,8 +13,6 @@ Pārlādēt ikonu Pārtraukt abonementu Atcelt - Dzēst birku - Pārsaukt Pārtraukt abonementu Atvērt iestatījumus Dzēst kontu @@ -23,7 +21,7 @@ Atzīmēt kā nelasītu Ik pēc %1$d minūtēm Atsvaidzināt ziņu plūsmas - Birkas + Birkas Ziņu plūsmas Pievienot ziņu plūsmu Tikko @@ -104,7 +102,7 @@ Motīvs Tumšs Izmantot sistēmas noklusējuma iestatījumus - Birkas + Birkas Izmantot lietotņu pārlūku Mazs Vidējs @@ -179,8 +177,7 @@ Dalīties Dalīties ar avārijas pierakstiem Saglabā avārijas pierakstus datnē, lai nosūtītu to izstrādātājam - Birkas - Pievienot vai atlasīt birku + Birkas Iespējot paziņojumus Atlasīt visu Iestatījumi diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 79e9369c4..d593581f8 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -24,7 +24,7 @@ Er du sikker på at du vil slette kontoen din? Handlingen kan ikke angres. Kun manuell Hvert %1$d. minutt - Etiketter + Etiketter Del artikkel Marker alle som lest Marker alle oppføringer som lest? @@ -65,7 +65,7 @@ Forkort titler Versjon Kopier versjonsnummer - Etiketter + Etiketter Bildeforhåndsvisning Systemstandard Skrifttype @@ -175,8 +175,7 @@ Last inn fullt innhold Standard Del kræsjlogger - Etiketter - Etikett + Etiketter Bekreft markering av alle som lest Veksle lest Veksle stjernemerking diff --git a/app/src/main/res/values-ne/strings.xml b/app/src/main/res/values-ne/strings.xml index 0b1677410..ce6f9a2f9 100644 --- a/app/src/main/res/values-ne/strings.xml +++ b/app/src/main/res/values-ne/strings.xml @@ -44,7 +44,7 @@ तल स्वाइप गर्नुहोस् अर्को लेखमा जानुहोस् पुष्टि गर्नुहोस् - ट्यागहरू + ट्यागहरू पुष्टि गर्नुहोस् अर्को लेख माथि स्वाइप गर्नुहोस् @@ -73,8 +73,7 @@ २ हप्ता १ हप्ता सधैंभरि - ट्याग - ट्यागहरू + ट्यागहरू सेटिङ्गहरु सेटिङ्गहरु नेभिगेसन ड्रयर खोल्नुहोस् @@ -98,7 +97,7 @@ के तपाइँ निश्चित हुनुहुन्छ कि तपाइँ लग आउट गर्न चाहनुहुन्छ? खाता मेट्नुहोस् खाता मेट्नुहोस् - ट्यागहरू + ट्यागहरू फीड थप्नुहोस् फिड फेला पार्न सकिएन फिड सुरक्षित गर्न सकिएन diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index ee9fdc73c..31c381a3a 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -41,14 +41,14 @@ Elke %d uur Alle feeds vernieuwen - Tags + Tags Zoekopdrachten Feeds Feed toevoegen Kon feed niet vinden Kon feed niet opslaan Kon feed niet vinden. Controleer je verbinding. - Fout bij verwijderen tag + Fout bij verwijderen tag Zojuist %1$dm %1$du @@ -127,7 +127,7 @@ Licht Donker Systeem standaard - Tags + Tags In-app browser gebruiken Stop Geen @@ -218,8 +218,7 @@ Delen Crash logs delen Bewaart crash logs naar een bestand om te delen met de ontwikkelaar - Tags - Tag toevoegen of selecteren + Tags Notificaties inschakelen Instellingen Notificatie toestemming is uitgeschakeld @@ -286,9 +285,9 @@ Categoriën Vandaag API Paswoord - Zichtbaarheid - Mainstream + Zichtbaarheid + Mainstream Belangrijk - Categorie + Categorie feed diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 46a958d9e..6ae150682 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -16,7 +16,7 @@ Powiadomienia O aplikacji Potwierdź - Kategorie + Kategorie Adres e-mail Prywatność Później @@ -29,17 +29,16 @@ Wyloguj się Wyloguj się Potwierdź - Kategorie + Kategorie %1$dd Feedbin FreshRSS Wyłączono Udostępnij - Dodaj lub wybierz kategorię Obrazy Nazwa uzytkownika Konto - Kategorie + Kategorie Wsparcie Ustawienia Motyw @@ -309,10 +308,10 @@ Użyj hasła zamiast tego Opcje zaawansowane Ukryj opcje zaawansowane - Widoczność - Główny strumień + Widoczność + Główny strumień Ważne - Kategoria + Kategoria Kanał Edytuj kategorię Odtwórz diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index af2b4331a..086fc5e99 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -40,7 +40,7 @@ A cada %d horas Atualizar Feeds - Etiquetas + Etiquetas Feeds Adicionar Feed Não foi possível encontrar o feed @@ -93,7 +93,7 @@ Claro Escuro Padrão do sistema - Etiquetas + Etiquetas Usar navegador interno Parar Nenhum @@ -141,7 +141,7 @@ Deslizar para Cima Próximo artigo Padrão - Etiquetas + Etiquetas Desmarcar Todas Abrir artigo no navegador Atualização periódica deve ser habilitada primeiro @@ -175,7 +175,6 @@ Avançado Experimental Reprodutor de áudio - Adicionar ou Selecionar Etiqueta Confirmar marcar todos como lidos Ir para próximo artigo Navegador @@ -238,24 +237,22 @@ Médio Deslizar para Cima Desabilitado - Erro ao excluir etiqueta + Erro ao excluir etiqueta Fixar Barras de Ferramentas Voltar um Nível Ir para o próximo feed Marcar como lido - Nome + Nome Falha ao compartilhar imagem Deslize para a esquerda ou a direita para navegar entre artigos Atualizar tudo Salvar Imagem salva - Excluir - Excluir + Excluir Falha ao salvar imagem - Tem certeza de que deseja excluir a etiqueta \"%1$s\"? - Etiqueta atualizada - Erro ao atualizar etiqueta - Editar Etiqueta + Tem certeza de que deseja excluir a etiqueta \"%1$s\"? + Etiqueta atualizada + Erro ao atualizar etiqueta Copiar link Compartilhar link Melhorar TalkBack @@ -295,10 +292,10 @@ Abrir Artigos no Navegador Selecionado API Senha - Visibilidade - Principal + Visibilidade + Principal Importante - Categoria + Categoria Feed Tocar Pausar diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 40e9195df..d0482b06b 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -22,7 +22,7 @@ Sair da sessão Cada %1$d minuto(s) Atualizar Feeds - Etiquetas + Etiquetas Pesquisas Feeds Adicionar Feed @@ -37,7 +37,7 @@ Claro Escuro Padrão do sistema - Etiquetas + Etiquetas Parar Médio Geral @@ -208,7 +208,7 @@ Partilhar Partilhar registos de falhas Gravar registos de falhas num ficheiro para partilhar com o programador - Etiquetas + Etiquetas Ativar notificações Configurações Desmarcar Todas @@ -237,8 +237,7 @@ Abrir Próximo Feed Feeds Pesquisar - Erro ao excluir etiqueta - Adicionar ou Selecionar Etiqueta + Erro ao excluir etiqueta Fixar no topo a barra de ferramentas A cada hora @@ -252,10 +251,9 @@ Você têm certeza que quer exluir a etiqueta \"%1$s\"? Excluir Navegar para Pai - Excluir - Etiqueta atualizada - Erro ao atualizar etiqueta - Editar etiqueta + Excluir + Etiqueta atualizada + Erro ao atualizar etiqueta Copiar link Melhorar o TalkBack Desativa algumas configurações para melhorar o TalkBack text-to-speech @@ -264,7 +262,7 @@ Ver mais artigos Títulos mais recentes Compartilhar link - Nome + Nome Saiba mais. Não tem uma instância do Miniflux? Encurtar títulos @@ -292,10 +290,10 @@ Categorias Modo Nada aqui - Visibilidade - Principal + Visibilidade + Principal Importante - Categoria + Categoria Feed Tocar Pausar diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index ecf44159b..ac8ec93df 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -46,11 +46,11 @@ La fiecare %d ore La fiecare %d de ore - Etichete + Etichete Nu s-a putut găsi un flux Nu s-a putut salva fluxul Nu am găsit niciun flux. Verifică conexiunea. - Eroare la ștergerea etichetei + Eroare la ștergerea etichetei %1$dz Distribuie articol Extrage conținutul complet @@ -115,7 +115,7 @@ Marchează ca citit Nume de utilizator Folosește browser-ul încorporat - Etichete + Etichete Luminoasă Începe importarea Se importă abonamentele… @@ -205,7 +205,6 @@ Mergi la următorul flux Dezactivat Distribuie log-urile de eroare - Adaugă sau alege etichete Activează notificări Șterge alegerea Alege tot @@ -221,7 +220,7 @@ Donează Dezactivat Mică - Etichete + Etichete Versiunea Implicit Anulează căutarea diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 60a35df91..289081305 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -21,7 +21,7 @@ Убрать из избранного Только вручную Обновить ленты - Теги + Теги Ленты Добавить ленту Лента добавлена @@ -110,7 +110,7 @@ Импортируется %1$d из %2$d подписок Импорт не удался Тёмная - Теги + Теги Использовать встроенный браузер Нет Маленький @@ -150,7 +150,7 @@ Сначала старые Сначала новые Поделиться - Теги + Теги Включить уведомления Настройки Выбрать все @@ -205,7 +205,6 @@ Создайте его здесь. У вас нет экземпляра FreshRSS? На вашем устройстве - Добавить или выбрать тег Прокрутка касанием (для E-ink) Включить высококонтрастную темную тему Коснитесь нижних углов, чтобы перемещаться по содержимому статьи @@ -296,11 +295,11 @@ Открыть статью в браузере Выбранно API-пароль - Видимость + Видимость Важный - Категория + Категория Лента - Основной поток + Основной поток Категории Режим Озвучить diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index d1cee8552..2c30e890e 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -77,7 +77,7 @@ %1$dh %1$dd Öppna åtgärder för artiklar - Etiketter + Etiketter Just nu Varje timma @@ -109,7 +109,7 @@ Har du inget Feedbin konto? Importera från fil Importerar prenumerationer %1$d av %2$d - Etiketter + Etiketter Avbryt Ingen Självhanterad hosting av Google Reader API @@ -180,8 +180,7 @@ Inställningar Välj inga Dela kraschloggar - Etiketter - Etikett + Etiketter Periodisk uppdatering måste vara aktiverad först Öppna navigationsfält Behörighet till aviseringar avaktiverad @@ -233,10 +232,10 @@ Inaktiverad Markera som läst Idag - Kategori + Kategori Flöde Viktigt - Synlighet + Synlighet API-lösenord Diverse Se fler artiklar diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 4888a4230..65789b128 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -15,8 +15,6 @@ குழுவிலகவும் சேமி ரத்துசெய் - குறிச்சொல்லை நீக்கு - மறுபெயரிடுங்கள் குழுவிலகவும் திறந்த அமைப்புகள் விடுபதிகை @@ -39,7 +37,7 @@ ஒவ்வொரு %d மணிநேரமும் ஊட்டங்களைப் புதுப்பிக்கவும் - குறிச்சொற்கள் + குறிச்சொற்கள் ஊட்டங்கள் ஊட்டத்தைச் சேர்க்கவும் ஊட்டத்தைக் கண்டுபிடிக்க முடியவில்லை @@ -92,7 +90,7 @@ ஒளி இருண்ட கணினி இயல்புநிலை - குறிச்சொற்கள் + குறிச்சொற்கள் பயன்பாட்டு உலாவியில் பயன்படுத்தவும் நிறுத்து எதுவுமில்லை @@ -171,8 +169,7 @@ திறந்த வழிசெலுத்தல் அலமாரியை செயலிழப்பு பதிவுகளைப் பகிரவும் டெவலப்பருடன் பகிர்ந்து கொள்ள ஒரு கோப்பில் செயலிழப்பு பதிவுகளை சேமிக்கிறது - குறிச்சொற்கள் - குறிச்சொல் சேர் அல்லது தேர்வுசெய் + குறிச்சொற்கள் அறிவிப்புகளை இயக்கவும் அமைப்புகள் அறிவிப்பு இசைவு முடக்கப்பட்டுள்ளது @@ -231,13 +228,13 @@ தேடல் படம் சேமிக்கப்பட்டது படத்தை சேமிப்பதில் தோல்வி - குறிச்சொல் புதுப்பிக்கப்பட்டது - குறிச்சொல்லைப் புதுப்பிப்பதில் பிழை - குறிச்சொல்லை நீக்குவதில் பிழை - நீக்கு + குறிச்சொல் புதுப்பிக்கப்பட்டது + குறிச்சொல்லைப் புதுப்பிப்பதில் பிழை + குறிச்சொல்லை நீக்குவதில் பிழை + நீக்கு உலாவியில் திறந்த கட்டுரை அனைத்தையும் படித்த பிறகு - நீக்கு + நீக்கு இணைப்பை நகலெடுக்கவும் இணைப்பைப் பகிரவும் தலைப்புச் செய்திகள் @@ -256,7 +253,7 @@ \"%1$s\" என்ற குறிச்சொல்லை நீக்க விரும்புகிறீர்களா? பெயர் அண்மைக் கால தலைப்புச் செய்திகள் - குறிச்சொல்லைத் திருத்து + குறிச்சொல்லைத் திருத்து படித்தபடி குறி சேமி பங்கு diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index b729ba905..e3fdf3f38 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -4,7 +4,7 @@ Ekle Yıldızlı Ayarları aç - Etiketler + Etiketler Yayın bulunamadı. Bağlantınızı kontrol edin. Tüm öğeleri okundu olarak işaretlensin mi? Parola @@ -17,7 +17,7 @@ Dışa aktar Abonelikler içe aktarılıyor %1$d / %2$d İçe aktarım ya başarısız oldu - Etiket + Etiket Uygulama içi tarayıcıyı kullan Durdur Metin boyutu @@ -66,7 +66,6 @@ Abonelikten çık Düzenle \"%1$s\" aboneliğinizi iptal etmek istediğinizden emin misiniz? - Etiketi Sil Abonelikten çık Şimdi Çıkış Yap @@ -125,7 +124,6 @@ Gizlilik Yayın eklendi Vazgeç - Yeniden Adlandır Hesabınızı silmek istediğinizden emin misiniz? Bu geri alınamaz. Ad kaydedildi Yayın bulunamadı @@ -182,7 +180,7 @@ Tümünü Seç Bildirim izni reddedildi Yayın Güncellemeleri - Etiketler + Etiketler Bildirimleri etkinleştir Ayarlar Önce düzenli yenileme etkinleştirilmelidir @@ -195,7 +193,6 @@ Sunucu FreshRSS Makaleyi tarayıcıda aç - Etiket Ekle ya da Seç Kaydırınca okundu olarak işaretle Yüksek karşıtlıklı karanlık temayı etkinleştir Tümünü Okundu Olarak İşaretle @@ -232,16 +229,16 @@ Kaydet Paylaş Hepsini yenile - Etiket güncellendi - Etiketi güncellemede hata + Etiket güncellendi + Etiketi güncellemede hata Bağlantıyı kopyala Bağlantıyı paylaş - Etiketi silmede hata - Sil - Sil - \"%1$s\" etiketini silmek istediğinizden emin misiniz? - Ad - Etiketi Düzenle + Etiketi silmede hata + Sil + Sil + \"%1$s\" etiketini silmek istediğinizden emin misiniz? + Ad + Etiketi Düzenle Yatay kaydırmayı etkinleştir Makaleler arasında gezinmek için sağa ya da sola kaydırın Başlıklar diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 41e4d5d41..bf0f8d040 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -43,14 +43,14 @@ Кожних %d годин Оновлювати стрічки - Теги + Теги Пошуки Стрічки Додати стрічку Не вдалося знайти стрічку Не вдалося зберегти стрічку Не вдалося знайти стрічку. Перевірте з\'єднання. - Помилка видалення тегу + Помилка видалення тегу Щойно %1$d хв %1$d год @@ -128,7 +128,7 @@ Світла Темна Типова системна - Теги + Теги Вбудований браузер Зупинити Вимкнено @@ -214,8 +214,7 @@ Поширити Експорт журналів збоїв Зберегти журнали збоїв до файлу для команди розробки - Теги - Додати чи вибрати тег + Теги Увімкнути сповіщення Параметри Сповіщення заборонено @@ -288,10 +287,10 @@ Категорії Сьогодні Пароль API - Головний потік - Категорія + Головний потік + Категорія Стрічка - Видимість + Видимість Важливе Відтворити Призупинити diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index eadbb073c..a1330e8ba 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -108,7 +108,7 @@ 每 %d 小时 - 标签 + 标签 分享文章 打开文章操作 设置 @@ -119,7 +119,7 @@ 上次更新 停止 - 标签 + 标签 浅色 系统默认 @@ -181,7 +181,7 @@ 必须首先启用定期刷新 全不选 通知 - 标签 + 标签 启用通知 设置 通知权限已禁用 @@ -200,7 +200,6 @@ 在这里创建一个。 没有 FreshRSS 实例? 了解更多。 - 添加或选择标签 freshrss.org 自托管的 Google Reader API 轻按电子墨水屏滚动 @@ -293,10 +292,10 @@ 今天 类别 API 密码 - 可见性 - 主信息流 + 可见性 + 主信息流 重要 - 类别 + 类别 订阅源 模式 播放 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index fd75a6300..3238ce75d 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -17,7 +17,7 @@ 每 %d 小時 重新整理訂閱源 - 標籤 + 標籤 訂閱源 新增訂閱源 無法找到訂閱源 @@ -119,7 +119,7 @@ 通知 匯入訂閲 開啟導覽匣 - 標籤 + 標籤 啟用通知 設定 必須先啟用定期重新整理 @@ -167,7 +167,7 @@ 從新增一個訂閱源開始 複製版本號 系統預設 - 標籤 + 標籤 使用應用程式内建瀏覽器 停止 @@ -202,7 +202,6 @@ 沒有 FreshRSS 實例? 在這裡建立一個。 了解更多。 - 新增或選擇標籤 點擊底部角落上下翻頁 E Ink 手勢 高對比度深色主題 @@ -298,18 +297,18 @@ 類別 日落 類別 - 類別 + 類別 API 權杖 改為使用密碼 改為使用 API 權杖 進階選項 隱藏進階選項 - 能見度 + 能見度 訂閱源 關閉播放器 倒轉 30 秒 快轉 30 秒 - 主資訊流 + 主資訊流 在瀏覽器中開啟文章 編輯類別 未讀徽章 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d4555a916..5cf88e695 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3,14 +3,18 @@ Capy Reader Feed or Website URL Name - New Tag + New Folder Add Feed added Cancel Starred + Saved for Later + Save for Later + Saved for Later Unread All Today + Home page Unsubscribe Edit Edit Feed @@ -19,8 +23,13 @@ Unsubscribe Save Cancel - Delete Tag + Delete Folder + Are you sure you want to delete tag \"%1$s\"? + Delete Rename + Tag updated + Error updating tag + Name Unsubscribe Open settings Log Out @@ -44,7 +53,7 @@ Every %d hours Refresh Feeds - Tags + Folders Searches My Labels Feeds @@ -63,7 +72,7 @@ Subscribe Feed added Page saved - Error deleting tag + Error deleting folder Just now %1$dm %1$dh @@ -154,7 +163,7 @@ Light Dark System default - Tags + Folders Use in-app browser Stop None @@ -174,6 +183,8 @@ No feeds yet Start by adding a feed Nothing here + Hide read articles + Show read articles Font style options Vollkorn Atkinson Hyperlegible @@ -256,8 +267,7 @@ Share Share crash logs Saves crash logs to a file to share with the developer - Tags - Add or Select Tag + Folders Enable notifications Settings Notification permission is disabled @@ -275,7 +285,6 @@ Enable high contrast dark theme Mark All As Read Browser - Categories Today Only show images on Wi-Fi Block images @@ -331,9 +340,8 @@ Visibility Main Stream Important - Category + Folder Feed - Edit Category Play Pause Close player diff --git a/bench/src/main/kotlin/com/jocmp/bench/Commands.kt b/bench/src/main/kotlin/com/jocmp/bench/Commands.kt index 684cb9a5c..cdb10f3b9 100644 --- a/bench/src/main/kotlin/com/jocmp/bench/Commands.kt +++ b/bench/src/main/kotlin/com/jocmp/bench/Commands.kt @@ -75,7 +75,7 @@ suspend fun commandFeeds(account: Account) { suspend fun commandArticles(account: Account) { val all = account.countAllByStatus(ArticleStatus.ALL).first() val unread = account.countAllByStatus(ArticleStatus.UNREAD).first() - val starred = account.countAllByStatus(ArticleStatus.STARRED).first() + val starred = account.countAllStarred().first() println("articles:") println(" all: $all") diff --git a/capy/src/main/java/com/jocmp/capy/Account.kt b/capy/src/main/java/com/jocmp/capy/Account.kt index c282d8e05..c82f2b484 100644 --- a/capy/src/main/java/com/jocmp/capy/Account.kt +++ b/capy/src/main/java/com/jocmp/capy/Account.kt @@ -61,11 +61,15 @@ data class Account( private val userAgent: String, private val acceptLanguage: String, private val localHttpClient: OkHttpClient = LocalOkHttpClient.forAccount(path = cacheDirectory), + private val extractUsername: String = "", + private val extractSecret: String = "", val delegate: AccountDelegate = when (source) { Source.LOCAL -> LocalAccountDelegate( database = database, httpClient = localHttpClient, preferences = preferences, + extractUsername = extractUsername, + extractSecret = extractSecret, ) Source.FEEDBIN -> FeedbinAccountDelegate( @@ -374,6 +378,10 @@ data class Account( return articleRecords.byStatus.count(status).asFlow().mapToOne(Dispatchers.IO) } + fun countAllStarred(): Flow { + return articleRecords.byStatus.countStarred().asFlow().mapToOne(Dispatchers.IO) + } + suspend fun dismissNotifications(ids: List) { articleRecords.dismissNotifications(ids) } diff --git a/capy/src/main/java/com/jocmp/capy/AccountManager.kt b/capy/src/main/java/com/jocmp/capy/AccountManager.kt index 51db22e0c..5d889f98b 100644 --- a/capy/src/main/java/com/jocmp/capy/AccountManager.kt +++ b/capy/src/main/java/com/jocmp/capy/AccountManager.kt @@ -17,6 +17,8 @@ class AccountManager( private val clientCertManager: ClientCertManager = ClientCertManager { builder, _ -> builder }, private val userAgent: String, private val acceptLanguage: String, + private val extractUsername: String = "", + private val extractSecret: String = "", ) { fun findByID( id: String, @@ -96,6 +98,8 @@ class AccountManager( clientCertManager = clientCertManager, userAgent = userAgent, acceptLanguage = acceptLanguage, + extractUsername = extractUsername, + extractSecret = extractSecret, ) } diff --git a/capy/src/main/java/com/jocmp/capy/Article.kt b/capy/src/main/java/com/jocmp/capy/Article.kt index 68fa3b5c8..9d96444a6 100644 --- a/capy/src/main/java/com/jocmp/capy/Article.kt +++ b/capy/src/main/java/com/jocmp/capy/Article.kt @@ -26,15 +26,12 @@ data class Article( val content: String = contentHTML.ifBlank { summary }, val enclosures: List = emptyList(), val enclosureType: EnclosureType? = null, + val isReadLater: Boolean = false, ) { val defaultContent = contentHTML.ifBlank { summary } val parseFullContent = fullContent == FullContentState.LOADED - val isPages: Boolean - get() = feedName == "Pages" && - feedURL?.startsWith("http://pages.feedbinusercontent.com/") == true - enum class FullContentState { NONE, LOADING, diff --git a/capy/src/main/java/com/jocmp/capy/ArticleFilter.kt b/capy/src/main/java/com/jocmp/capy/ArticleFilter.kt index a9cbc53a5..c2d7d816b 100644 --- a/capy/src/main/java/com/jocmp/capy/ArticleFilter.kt +++ b/capy/src/main/java/com/jocmp/capy/ArticleFilter.kt @@ -24,6 +24,10 @@ sealed class ArticleFilter(open val status: ArticleStatus) { return this is Today } + fun hasStarredSelected(): Boolean { + return this is Starred + } + fun withStatus(status: ArticleStatus): ArticleFilter { return when (this) { is Articles -> copy(articleStatus = status) @@ -31,6 +35,7 @@ sealed class ArticleFilter(open val status: ArticleStatus) { is Folders -> copy(folderStatus = status) is SavedSearches -> copy(savedSearchStatus = status) is Today -> copy(todayStatus = status) + is Starred -> copy(starredStatus = status) } } @@ -69,6 +74,12 @@ sealed class ArticleFilter(open val status: ArticleStatus) { get() = todayStatus } + @Serializable + data class Starred(val starredStatus: ArticleStatus = ArticleStatus.ALL) : ArticleFilter(starredStatus) { + override val status: ArticleStatus + get() = starredStatus + } + companion object { fun default() = Articles(articleStatus = ArticleStatus.ALL) } diff --git a/capy/src/main/java/com/jocmp/capy/ArticleStatus.kt b/capy/src/main/java/com/jocmp/capy/ArticleStatus.kt index 6dbd78f40..8dae97f75 100644 --- a/capy/src/main/java/com/jocmp/capy/ArticleStatus.kt +++ b/capy/src/main/java/com/jocmp/capy/ArticleStatus.kt @@ -6,5 +6,4 @@ import kotlinx.serialization.Serializable enum class ArticleStatus { ALL, UNREAD, - STARRED } diff --git a/capy/src/main/java/com/jocmp/capy/Feed.kt b/capy/src/main/java/com/jocmp/capy/Feed.kt index 2af75d66b..4fecaabde 100644 --- a/capy/src/main/java/com/jocmp/capy/Feed.kt +++ b/capy/src/main/java/com/jocmp/capy/Feed.kt @@ -20,7 +20,5 @@ data class Feed( val folderExpanded: Boolean = false, val priority: FeedPriority? = null, val showUnreadBadge: Boolean = true, -): Countable { - val isPages: Boolean - get() = feedURL.startsWith("http://pages.feedbinusercontent.com/") -} + val isReadLater: Boolean = false, +): Countable diff --git a/capy/src/main/java/com/jocmp/capy/accounts/Source.kt b/capy/src/main/java/com/jocmp/capy/accounts/Source.kt index 8954d3ee4..2dc8ed460 100644 --- a/capy/src/main/java/com/jocmp/capy/accounts/Source.kt +++ b/capy/src/main/java/com/jocmp/capy/accounts/Source.kt @@ -35,8 +35,8 @@ enum class Source(val value: String) { val supportsTagDeletion get() = !isMiniflux - val supportsPages - get() = this == FEEDBIN + val supportsReadLater + get() = this == FEEDBIN || this == LOCAL private val isMiniflux get() = this == MINIFLUX || this == MINIFLUX_TOKEN diff --git a/capy/src/main/java/com/jocmp/capy/accounts/feedbin/FeedbinAccountDelegate.kt b/capy/src/main/java/com/jocmp/capy/accounts/feedbin/FeedbinAccountDelegate.kt index 949cb7f0b..4456324bf 100644 --- a/capy/src/main/java/com/jocmp/capy/accounts/feedbin/FeedbinAccountDelegate.kt +++ b/capy/src/main/java/com/jocmp/capy/accounts/feedbin/FeedbinAccountDelegate.kt @@ -301,6 +301,7 @@ internal class FeedbinAccountDelegate( favicon_url = icon?.url, priority = null, itunes_image_url = null, + read_later = subscription.feed_url.startsWith("http://pages.feedbinusercontent.com/"), ) } @@ -379,7 +380,7 @@ internal class FeedbinAccountDelegate( private suspend fun refreshPages() { val feedID = database.feedsQueries - .findPagesFeedID() + .findReadLaterFeedID() .executeAsOneOrNull() ?: return val remoteIDs = fetchAllFeedEntryIDs(feedID) diff --git a/capy/src/main/java/com/jocmp/capy/accounts/local/LocalAccountDelegate.kt b/capy/src/main/java/com/jocmp/capy/accounts/local/LocalAccountDelegate.kt index 3f2e692b9..832842fa4 100644 --- a/capy/src/main/java/com/jocmp/capy/accounts/local/LocalAccountDelegate.kt +++ b/capy/src/main/java/com/jocmp/capy/accounts/local/LocalAccountDelegate.kt @@ -16,6 +16,7 @@ import com.jocmp.capy.persistence.ArticleRecords import com.jocmp.capy.persistence.EnclosureRecords import com.jocmp.capy.persistence.FeedRecords import com.jocmp.capy.persistence.TaggingRecords +import com.jocmp.feedbinclient.MercuryParser import com.jocmp.feedfinder.DefaultFeedFinder import com.jocmp.feedfinder.FeedFinder import com.jocmp.rssparser.model.RssItem @@ -32,7 +33,16 @@ internal class LocalAccountDelegate( private val httpClient: OkHttpClient, private val feedFinder: FeedFinder = DefaultFeedFinder(httpClient), private val preferences: AccountPreferences, + private val extractUsername: String = "", + private val extractSecret: String = "", ) : AccountDelegate { + private val mercuryParser + get() = MercuryParser( + username = extractUsername, + secret = extractSecret, + httpClient = httpClient, + ) + private val feedRecords = FeedRecords(database) private val articleRecords = ArticleRecords(database) private val taggingRecords = TaggingRecords(database) @@ -49,8 +59,52 @@ internal class LocalAccountDelegate( return Result.success(Unit) } - override suspend fun createPage(url: String) = - Result.failure(UnsupportedOperationException("Pages not supported")) + override suspend fun createPage(url: String): Result { + val feedID = findOrCreateReadLaterFeed() + val page = + mercuryParser.parse(url) ?: return Result.failure(Throwable("Failed to fetch page")) + val updatedAt = nowUTC() + + database.transactionWithErrorHandling { + database.articlesQueries.create( + id = url, + feed_id = feedID, + title = page.title ?: url, + author = page.author, + content_html = page.content, + url = url, + summary = page.excerpt, + extracted_content_url = null, + image_url = page.lead_image_url, + published_at = updatedAt.toEpochSecond(), + enclosure_type = null, + ) + + articleRecords.createStatus( + articleID = url, + updatedAt = updatedAt, + read = false, + ) + } + + return Result.success(Unit) + } + + private fun findOrCreateReadLaterFeed(): String { + database.feedsQueries.upsert( + id = READ_LATER_FEED_URL, + subscription_id = READ_LATER_FEED_URL, + title = "Saved for Later", + feed_url = READ_LATER_FEED_URL, + site_url = null, + favicon_url = null, + priority = null, + itunes_image_url = null, + read_later = true, + ) + + return READ_LATER_FEED_URL + } override suspend fun addFeed( url: String, @@ -138,6 +192,11 @@ internal class LocalAccountDelegate( } + override suspend fun deletePage(articleID: String): Result { + database.articlesQueries.deletePageByID(articleID) + return Result.success(Unit) + } + override suspend fun addStar(articleIDs: List): Result { return Result.success(Unit) } @@ -189,7 +248,7 @@ internal class LocalAccountDelegate( } private suspend fun refreshArticleFilter(cutoffDate: ZonedDateTime?) { - val feeds = feedRecords.feeds().firstOrNull() ?: return + val feeds = feedRecords.feeds().firstOrNull()?.filterNot { it.isReadLater } ?: return refreshFeeds(feeds, cutoffDate = cutoffDate) } @@ -316,6 +375,7 @@ internal class LocalAccountDelegate( favicon_url = feed.faviconURL?.toString(), priority = null, itunes_image_url = feed.itunesImageURL, + read_later = false, ) } @@ -337,6 +397,7 @@ internal class LocalAccountDelegate( private fun tag(path: String) = "$TAG.$path" private const val TAG = "local" + private const val READ_LATER_FEED_URL = "https://pages.capyreader.com" } } diff --git a/capy/src/main/java/com/jocmp/capy/accounts/miniflux/MinifluxAccountDelegate.kt b/capy/src/main/java/com/jocmp/capy/accounts/miniflux/MinifluxAccountDelegate.kt index bcd70327f..0cae430aa 100644 --- a/capy/src/main/java/com/jocmp/capy/accounts/miniflux/MinifluxAccountDelegate.kt +++ b/capy/src/main/java/com/jocmp/capy/accounts/miniflux/MinifluxAccountDelegate.kt @@ -441,6 +441,7 @@ internal class MinifluxAccountDelegate( favicon_url = icon, priority = null, itunes_image_url = null, + read_later = false, ) feed.category?.let { category -> diff --git a/capy/src/main/java/com/jocmp/capy/accounts/reader/ReaderAccountDelegate.kt b/capy/src/main/java/com/jocmp/capy/accounts/reader/ReaderAccountDelegate.kt index 0ba42e04a..9e61ebcaf 100644 --- a/capy/src/main/java/com/jocmp/capy/accounts/reader/ReaderAccountDelegate.kt +++ b/capy/src/main/java/com/jocmp/capy/accounts/reader/ReaderAccountDelegate.kt @@ -318,6 +318,7 @@ internal class ReaderAccountDelegate( }, priority = subscription.frssPriority, itunes_image_url = null, + read_later = false, ) upsertTaggings(subscription) @@ -663,7 +664,7 @@ private val SubscriptionQuickAddResult.toSubscription: Subscription? private fun ArticleFilter.toStream(source: Source): Stream { return when (this) { - is ArticleFilter.Articles, is ArticleFilter.Today -> Read() + is ArticleFilter.Articles, is ArticleFilter.Today, is ArticleFilter.Starred -> Read() is ArticleFilter.Feeds -> Stream.Feed(feedID) is ArticleFilter.Folders -> folderStream(this, source) is ArticleFilter.SavedSearches -> UserLabel(savedSearchID) diff --git a/capy/src/main/java/com/jocmp/capy/articles/ArticleRenderer.kt b/capy/src/main/java/com/jocmp/capy/articles/ArticleRenderer.kt index 88857c85a..df587c6e8 100644 --- a/capy/src/main/java/com/jocmp/capy/articles/ArticleRenderer.kt +++ b/capy/src/main/java/com/jocmp/capy/articles/ArticleRenderer.kt @@ -20,21 +20,22 @@ class ArticleRenderer( byline: String, colors: Map, hideImages: Boolean, + feedName: String = article.feedName, ): String { val fontFamily = fontOption.get() val showPlaceholderTitle = article.title.isBlank() val enableHorizontalScroll = enableHorizontalScroll.get() val title = if (showPlaceholderTitle) { - article.feedName + feedName } else { article.title } - val feedName = if (showPlaceholderTitle) { + val displayFeedName = if (showPlaceholderTitle) { "" } else { - article.feedName + feedName } val content = buildContent(article, hideImages) @@ -49,7 +50,7 @@ class ArticleRenderer( "external_link" to article.externalLink(), "title" to title, "byline" to byline, - "feed_name" to feedName, + "feed_name" to displayFeedName, "font_size" to "${textSize.get()}px", "font_family" to fontFamily.slug, "font_preload" to fontPreload(fontFamily), diff --git a/capy/src/main/java/com/jocmp/capy/articles/NextFilter.kt b/capy/src/main/java/com/jocmp/capy/articles/NextFilter.kt index f590a4c6f..85fc09776 100644 --- a/capy/src/main/java/com/jocmp/capy/articles/NextFilter.kt +++ b/capy/src/main/java/com/jocmp/capy/articles/NextFilter.kt @@ -118,7 +118,7 @@ sealed class NextFilter { } is ArticleFilter.Feeds -> findNextFeed(filter, folders, feeds) - is ArticleFilter.Today -> { + is ArticleFilter.Today, is ArticleFilter.Starred -> { val firstFeed = feeds.firstOrNull() val firstFolder = folders.firstOrNull() val firstSearch = searches.firstOrNull() diff --git a/capy/src/main/java/com/jocmp/capy/persistence/ArticleMapper.kt b/capy/src/main/java/com/jocmp/capy/persistence/ArticleMapper.kt index e39721a6e..1e387f8c0 100644 --- a/capy/src/main/java/com/jocmp/capy/persistence/ArticleMapper.kt +++ b/capy/src/main/java/com/jocmp/capy/persistence/ArticleMapper.kt @@ -23,6 +23,7 @@ internal fun articleMapper( openInBrowser: Boolean, feedURL: String?, siteURL: String?, + readLater: Boolean, updatedAt: Long?, starred: Boolean, read: Boolean, @@ -47,6 +48,7 @@ internal fun articleMapper( enableStickyFullContent = enableStickyContent, openInBrowser = openInBrowser, enclosureType = EnclosureType.from(enclosureType), + isReadLater = readLater, ) } @@ -63,6 +65,7 @@ internal fun listMapper( feedTitle: String?, faviconURL: String?, openInBrowser: Boolean, + readLater: Boolean, updatedAt: Long?, starred: Boolean?, read: Boolean?, @@ -91,6 +94,7 @@ internal fun listMapper( openInBrowser = openInBrowser, feedURL = null, siteURL = null, + readLater = readLater, updatedAt = updatedAt, starred = starred ?: false, read = read ?: false, diff --git a/capy/src/main/java/com/jocmp/capy/persistence/ArticleRecords.kt b/capy/src/main/java/com/jocmp/capy/persistence/ArticleRecords.kt index 6056b201b..cb3a0b260 100644 --- a/capy/src/main/java/com/jocmp/capy/persistence/ArticleRecords.kt +++ b/capy/src/main/java/com/jocmp/capy/persistence/ArticleRecords.kt @@ -294,6 +294,11 @@ class ArticleRecords( query = query, since = null ) + + is ArticleFilter.Starred -> byStatus.countStarred( + status = filter.status, + query = query, + ) } return count.asFlow().mapToOneOrDefault(0L, Dispatchers.IO) @@ -368,6 +373,13 @@ class ArticleRecords( sortOrder = sortOrder, query = query, ) + + is ArticleFilter.Starred -> byStatus.unreadArticleIDs( + ArticleStatus.ALL, + range = range, + sortOrder = sortOrder, + query = query, + ) } return ids.executeAsList() diff --git a/capy/src/main/java/com/jocmp/capy/persistence/ArticleStatusPair.kt b/capy/src/main/java/com/jocmp/capy/persistence/ArticleStatusPair.kt index f8740cdb7..4a00727e4 100644 --- a/capy/src/main/java/com/jocmp/capy/persistence/ArticleStatusPair.kt +++ b/capy/src/main/java/com/jocmp/capy/persistence/ArticleStatusPair.kt @@ -11,11 +11,7 @@ internal val ArticleStatus.toStatusPair: ArticleStatusPair get() = when(this) { ArticleStatus.ALL -> ArticleStatusPair(read = null, starred = null) ArticleStatus.UNREAD -> ArticleStatusPair(read = false, starred = null) - ArticleStatus.STARRED -> ArticleStatusPair(read = null, starred = true) } internal val ArticleStatus.forCounts: ArticleStatusPair - get() = when(this) { - ArticleStatus.STARRED -> ArticleStatusPair(read = null, starred = true) - else -> ArticleStatusPair(read = false, starred = null) - } + get() = ArticleStatusPair(read = false, starred = null) diff --git a/capy/src/main/java/com/jocmp/capy/persistence/FeedRecords.kt b/capy/src/main/java/com/jocmp/capy/persistence/FeedRecords.kt index 87b94fcde..263154b04 100644 --- a/capy/src/main/java/com/jocmp/capy/persistence/FeedRecords.kt +++ b/capy/src/main/java/com/jocmp/capy/persistence/FeedRecords.kt @@ -29,6 +29,7 @@ internal class FeedRecords(private val database: Database) { faviconURL: String?, priority: String? = null, itunesImageURL: String? = null, + readLater: Boolean = false, ): Feed? = withIOContext { database.feedsQueries.upsert( id = feedID, @@ -39,6 +40,7 @@ internal class FeedRecords(private val database: Database) { favicon_url = faviconURL, priority = priority, itunes_image_url = itunesImageURL, + read_later = readLater, ) find(feedID) @@ -148,6 +150,7 @@ internal class FeedRecords(private val database: Database) { priority: String? = null, showUnreadBadge: Boolean = true, itunesImageURL: String? = null, + readLater: Boolean = false, folderName: String? = "", expanded: Boolean? = false, ) = Feed( @@ -166,5 +169,6 @@ internal class FeedRecords(private val database: Database) { folderExpanded = expanded ?: false, priority = FeedPriority.parse(priority), showUnreadBadge = showUnreadBadge, + isReadLater = readLater, ) } diff --git a/capy/src/main/java/com/jocmp/capy/persistence/articles/ByArticleStatus.kt b/capy/src/main/java/com/jocmp/capy/persistence/articles/ByArticleStatus.kt index 55f382574..9a16fd72c 100644 --- a/capy/src/main/java/com/jocmp/capy/persistence/articles/ByArticleStatus.kt +++ b/capy/src/main/java/com/jocmp/capy/persistence/articles/ByArticleStatus.kt @@ -88,4 +88,58 @@ class ByArticleStatus(private val database: Database) { publishedSince = null ) } + + fun countStarred( + status: ArticleStatus = ArticleStatus.ALL, + query: String? = null, + ): Query { + val (read, _) = status.toStatusPair + + return database.articlesByStatusQueries.countAll( + read = read, + starred = true, + query = query, + lastReadAt = null, + lastUnstarredAt = null, + publishedSince = null + ) + } + + fun allStarred( + status: ArticleStatus = ArticleStatus.ALL, + query: String? = null, + limit: Long, + offset: Long, + sortOrder: SortOrder, + since: OffsetDateTime? = null, + ): Query
{ + val (read, _) = status.toStatusPair + val queries = database.articlesByStatusQueries + + return if (isNewestFirst(sortOrder)) { + queries.allNewestFirst( + read = read, + starred = true, + limit = limit, + offset = offset, + lastReadAt = mapLastRead(read, since), + lastUnstarredAt = mapLastUnstarred(true, since), + publishedSince = null, + query = query, + mapper = ::listMapper + ) + } else { + queries.allOldestFirst( + read = read, + starred = true, + limit = limit, + offset = offset, + lastReadAt = mapLastRead(read, since), + lastUnstarredAt = mapLastUnstarred(true, since), + publishedSince = null, + query = query, + mapper = ::listMapper + ) + } + } } diff --git a/capy/src/main/sqldelight/com/jocmp/capy/db/24_AddReadLaterToFeeds.sqm b/capy/src/main/sqldelight/com/jocmp/capy/db/24_AddReadLaterToFeeds.sqm new file mode 100644 index 000000000..b3d32159b --- /dev/null +++ b/capy/src/main/sqldelight/com/jocmp/capy/db/24_AddReadLaterToFeeds.sqm @@ -0,0 +1,5 @@ +import kotlin.Boolean; + +ALTER TABLE feeds ADD COLUMN read_later INTEGER AS Boolean NOT NULL DEFAULT 0; + +CREATE UNIQUE INDEX feeds_read_later_unique ON feeds(read_later) WHERE read_later = 1; diff --git a/capy/src/main/sqldelight/com/jocmp/capy/db/articles.sq b/capy/src/main/sqldelight/com/jocmp/capy/db/articles.sq index 15e3fc9a1..0d27f48fe 100644 --- a/capy/src/main/sqldelight/com/jocmp/capy/db/articles.sq +++ b/capy/src/main/sqldelight/com/jocmp/capy/db/articles.sq @@ -7,6 +7,7 @@ SELECT feeds.open_articles_in_browser, feeds.feed_url, feeds.site_url, + feeds.read_later, article_statuses.updated_at, article_statuses.starred, article_statuses.read @@ -245,7 +246,7 @@ deleteArticles { AND NOT EXISTS ( SELECT 1 FROM feeds WHERE feeds.id = articles.feed_id - AND feeds.feed_url LIKE 'http://pages.feedbinusercontent.com/%' + AND feeds.read_later = 1 ); DELETE FROM article_notifications diff --git a/capy/src/main/sqldelight/com/jocmp/capy/db/articlesByFeed.sq b/capy/src/main/sqldelight/com/jocmp/capy/db/articlesByFeed.sq index 85e330c47..1e4f47fd3 100644 --- a/capy/src/main/sqldelight/com/jocmp/capy/db/articlesByFeed.sq +++ b/capy/src/main/sqldelight/com/jocmp/capy/db/articlesByFeed.sq @@ -12,6 +12,7 @@ SELECT feeds.title AS feed_title, feeds.favicon_url, feeds.open_articles_in_browser, + feeds.read_later, article_statuses.updated_at, article_statuses.starred, article_statuses.read @@ -41,6 +42,7 @@ SELECT feeds.title AS feed_title, feeds.favicon_url, feeds.open_articles_in_browser, + feeds.read_later, article_statuses.updated_at, article_statuses.starred, article_statuses.read diff --git a/capy/src/main/sqldelight/com/jocmp/capy/db/articlesBySavedSearch.sq b/capy/src/main/sqldelight/com/jocmp/capy/db/articlesBySavedSearch.sq index 26c3c370a..d3a41d6bb 100644 --- a/capy/src/main/sqldelight/com/jocmp/capy/db/articlesBySavedSearch.sq +++ b/capy/src/main/sqldelight/com/jocmp/capy/db/articlesBySavedSearch.sq @@ -12,6 +12,7 @@ SELECT feeds.title AS feed_title, feeds.favicon_url, feeds.open_articles_in_browser, + feeds.read_later, article_statuses.updated_at, article_statuses.starred, article_statuses.read @@ -41,6 +42,7 @@ SELECT feeds.title AS feed_title, feeds.favicon_url, feeds.open_articles_in_browser, + feeds.read_later, article_statuses.updated_at, article_statuses.starred, article_statuses.read diff --git a/capy/src/main/sqldelight/com/jocmp/capy/db/articlesByStatus.sq b/capy/src/main/sqldelight/com/jocmp/capy/db/articlesByStatus.sq index 96b62a8aa..47119b5eb 100644 --- a/capy/src/main/sqldelight/com/jocmp/capy/db/articlesByStatus.sq +++ b/capy/src/main/sqldelight/com/jocmp/capy/db/articlesByStatus.sq @@ -12,6 +12,7 @@ SELECT feeds.title AS feed_title, feeds.favicon_url, feeds.open_articles_in_browser, + feeds.read_later, article_statuses.updated_at, article_statuses.starred, article_statuses.read @@ -20,6 +21,7 @@ JOIN feeds ON articles.feed_id = feeds.id JOIN article_statuses ON articles.id = article_statuses.article_id WHERE ((article_statuses.read = :read AND article_statuses.last_read_at IS NULL OR article_statuses.last_read_at >= :lastReadAt) OR :read IS NULL) AND (feeds.priority IS NULL OR feeds.priority IN ('main', 'important')) +AND feeds.read_later = 0 AND ((article_statuses.starred = :starred AND article_statuses.last_unstarred_at IS NULL OR article_statuses.last_unstarred_at >= :lastUnstarredAt) OR :starred IS NULL) AND (articles.published_at >= :publishedSince OR :publishedSince IS NULL) AND (:query IS NULL OR articles.title LIKE '%' || :query || '%' OR articles.summary LIKE '%' || :query || '%') @@ -40,6 +42,7 @@ SELECT feeds.title AS feed_title, feeds.favicon_url, feeds.open_articles_in_browser, + feeds.read_later, article_statuses.updated_at, article_statuses.starred, article_statuses.read @@ -48,6 +51,7 @@ JOIN feeds ON articles.feed_id = feeds.id JOIN article_statuses ON articles.id = article_statuses.article_id WHERE ((article_statuses.read = :read AND article_statuses.last_read_at IS NULL OR article_statuses.last_read_at >= :lastReadAt) OR :read IS NULL) AND (feeds.priority IS NULL OR feeds.priority IN ('main', 'important')) +AND feeds.read_later = 0 AND ((article_statuses.starred = :starred AND article_statuses.last_unstarred_at IS NULL OR article_statuses.last_unstarred_at >= :lastUnstarredAt) OR :starred IS NULL) AND (articles.published_at >= :publishedSince OR :publishedSince IS NULL) AND (:query IS NULL OR articles.title LIKE '%' || :query || '%' OR articles.summary LIKE '%' || :query || '%') @@ -61,6 +65,7 @@ JOIN feeds ON articles.feed_id = feeds.id JOIN article_statuses ON articles.id = article_statuses.article_id WHERE ((article_statuses.read = :read AND article_statuses.last_read_at IS NULL OR article_statuses.last_read_at >= :lastReadAt) OR :read IS NULL) AND (feeds.priority IS NULL OR feeds.priority IN ('main', 'important')) +AND feeds.read_later = 0 AND ((article_statuses.starred = :starred AND article_statuses.last_unstarred_at IS NULL OR article_statuses.last_unstarred_at >= :lastUnstarredAt) OR :starred IS NULL) AND (articles.published_at >= :publishedSince OR :publishedSince IS NULL) AND (:query IS NULL OR articles.title LIKE '%' || :query || '%' OR articles.summary LIKE '%' || :query || '%'); @@ -73,6 +78,7 @@ JOIN article_statuses ON articles.id = article_statuses.article_id WHERE article_statuses.read = 0 AND (article_statuses.starred = :starred OR :starred IS NULL) AND (feeds.priority IS NULL OR feeds.priority IN ('main', 'important')) +AND feeds.read_later = 0 AND (articles.published_at >= :publishedSince OR :publishedSince IS NULL) AND (:query IS NULL OR articles.title LIKE '%' || :query || '%' OR articles.summary LIKE '%' || :query || '%') AND ( diff --git a/capy/src/main/sqldelight/com/jocmp/capy/db/feeds.sq b/capy/src/main/sqldelight/com/jocmp/capy/db/feeds.sq index 13b34f7bc..411f95ae8 100644 --- a/capy/src/main/sqldelight/com/jocmp/capy/db/feeds.sq +++ b/capy/src/main/sqldelight/com/jocmp/capy/db/feeds.sq @@ -25,10 +25,10 @@ FROM feeds WHERE feed_url = :feedURL LIMIT 1; -findPagesFeedID: +findReadLaterFeedID: SELECT id FROM feeds -WHERE feed_url LIKE 'http://pages.feedbinusercontent.com/%' +WHERE read_later = 1 LIMIT 1; findByFolder: @@ -46,7 +46,8 @@ INSERT INTO feeds( site_url, favicon_url, priority, - itunes_image_url + itunes_image_url, + read_later ) VALUES ( :id, @@ -56,7 +57,8 @@ VALUES ( :site_url, :favicon_url, :priority, - :itunes_image_url + :itunes_image_url, + :read_later ) ON CONFLICT(id) DO UPDATE SET id = id, @@ -67,7 +69,8 @@ site_url = excluded.site_url, favicon_url = COALESCE(excluded.favicon_url, favicon_url), enable_sticky_full_content = enable_sticky_full_content, priority = excluded.priority, -itunes_image_url = COALESCE(excluded.itunes_image_url, itunes_image_url); +itunes_image_url = COALESCE(excluded.itunes_image_url, itunes_image_url), +read_later = excluded.read_later; update: UPDATE feeds SET diff --git a/capy/src/test/java/com/jocmp/capy/ArticleFilterTest.kt b/capy/src/test/java/com/jocmp/capy/ArticleFilterTest.kt index 69196b480..eea167326 100644 --- a/capy/src/test/java/com/jocmp/capy/ArticleFilterTest.kt +++ b/capy/src/test/java/com/jocmp/capy/ArticleFilterTest.kt @@ -9,9 +9,9 @@ class ArticleFilterTest { fun withStatus_copiesExistingFilter() { val articles = ArticleFilter.default() - val nextFilter = articles.withStatus(status = ArticleStatus.STARRED) + val nextFilter = articles.withStatus(status = ArticleStatus.UNREAD) assertNotEquals(articles.status, nextFilter.status) - assertEquals(expected = ArticleStatus.STARRED, actual = nextFilter.status) + assertEquals(expected = ArticleStatus.UNREAD, actual = nextFilter.status) } } diff --git a/capy/src/test/java/com/jocmp/capy/accounts/feedbin/FeedbinAccountDelegateTest.kt b/capy/src/test/java/com/jocmp/capy/accounts/feedbin/FeedbinAccountDelegateTest.kt index b5ce23bae..26acb90f3 100644 --- a/capy/src/test/java/com/jocmp/capy/accounts/feedbin/FeedbinAccountDelegateTest.kt +++ b/capy/src/test/java/com/jocmp/capy/accounts/feedbin/FeedbinAccountDelegateTest.kt @@ -275,8 +275,7 @@ class FeedbinAccountDelegateTest { val starredArticles = ArticleRecords(database) .byStatus - .all( - ArticleStatus.STARRED, + .allStarred( limit = 2, offset = 0, sortOrder = SortOrder.NEWEST_FIRST, diff --git a/capy/src/test/java/com/jocmp/capy/accounts/reader/ReaderAccountDelegateTest.kt b/capy/src/test/java/com/jocmp/capy/accounts/reader/ReaderAccountDelegateTest.kt index 47bebacd5..ee6655df5 100644 --- a/capy/src/test/java/com/jocmp/capy/accounts/reader/ReaderAccountDelegateTest.kt +++ b/capy/src/test/java/com/jocmp/capy/accounts/reader/ReaderAccountDelegateTest.kt @@ -339,8 +339,7 @@ class ReaderAccountDelegateTest { val starredArticles = ArticleRecords(database) .byStatus - .all( - ArticleStatus.STARRED, + .allStarred( limit = 2, offset = 0, sortOrder = SortOrder.NEWEST_FIRST, 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 de21cb5e6..2e81bb625 100644 --- a/capy/src/test/java/com/jocmp/capy/fixtures/FeedFixture.kt +++ b/capy/src/test/java/com/jocmp/capy/fixtures/FeedFixture.kt @@ -18,6 +18,7 @@ internal class FeedFixture( title: String = "My Feed", folderNames: List = emptyList(), enableNotifications: Boolean = false, + readLater: Boolean = false, ): Feed = runBlocking { val feed = records.upsert( feedID = feedID, @@ -26,6 +27,7 @@ internal class FeedFixture( feedURL = feedURL, siteURL = feedURL, faviconURL = null, + readLater = readLater, )!! if (enableNotifications) { diff --git a/capy/src/test/java/com/jocmp/capy/persistence/ArticleMapperTest.kt b/capy/src/test/java/com/jocmp/capy/persistence/ArticleMapperTest.kt index 2110fc4a2..1396ab9d7 100644 --- a/capy/src/test/java/com/jocmp/capy/persistence/ArticleMapperTest.kt +++ b/capy/src/test/java/com/jocmp/capy/persistence/ArticleMapperTest.kt @@ -26,6 +26,7 @@ class ArticleMapperTest { openInBrowser = false, feedURL = null, siteURL = null, + readLater = false, updatedAt = 1703960809, starred = false, read = false, diff --git a/capy/src/test/java/com/jocmp/capy/persistence/ArticleRecordsTest.kt b/capy/src/test/java/com/jocmp/capy/persistence/ArticleRecordsTest.kt index 97e78680a..7343553c1 100644 --- a/capy/src/test/java/com/jocmp/capy/persistence/ArticleRecordsTest.kt +++ b/capy/src/test/java/com/jocmp/capy/persistence/ArticleRecordsTest.kt @@ -344,8 +344,7 @@ class ArticleRecordsTest { val results = articleRecords .byStatus - .all( - status = ArticleStatus.STARRED, + .allStarred( limit = 10, offset = 0, sortOrder = SortOrder.NEWEST_FIRST, @@ -638,6 +637,39 @@ class ArticleRecordsTest { assertEquals(expected = 3, actual = counts[firstSearch.id]) assertEquals(expected = 2, actual = counts[secondSearch.id]) } + + @Test + fun allByStatus_excludesReadLaterArticles() { + val feedFixture = FeedFixture(database) + val readLaterFeed = feedFixture.create( + feedURL = "https://pages.capyreader.com", + readLater = true, + ) + val regularFeed = feedFixture.create( + feedURL = "https://example.com/${RandomUUID.generate()}", + ) + + articleFixture.create(feed = readLaterFeed, read = false) + val regularArticle = articleFixture.create(feed = regularFeed, read = false) + + val results = articleRecords + .byStatus + .all( + ArticleStatus.UNREAD, + limit = 10, + offset = 0, + sortOrder = SortOrder.NEWEST_FIRST, + ) + .executeAsList() + + val count = articleRecords + .byStatus + .count(ArticleStatus.UNREAD) + .executeAsOne() + + assertEquals(expected = listOf(regularArticle.id), actual = results.map { it.id }) + assertEquals(expected = 1, actual = count) + } } fun sortedMessage(expected: List
, actual: List
): String { diff --git a/capy/src/test/java/com/jocmp/capy/persistence/ArticleStatusPairTest.kt b/capy/src/test/java/com/jocmp/capy/persistence/ArticleStatusPairTest.kt index 2038c7cb2..a5428cf3e 100644 --- a/capy/src/test/java/com/jocmp/capy/persistence/ArticleStatusPairTest.kt +++ b/capy/src/test/java/com/jocmp/capy/persistence/ArticleStatusPairTest.kt @@ -4,7 +4,6 @@ import com.jocmp.capy.ArticleStatus import org.junit.Test import kotlin.test.assertFalse import kotlin.test.assertNull -import kotlin.test.assertTrue class ArticleStatusPairTest { @Test @@ -23,11 +22,4 @@ class ArticleStatusPairTest { assertNull(starred) } - @Test - fun toStatusPair_starredStatus() { - val (read, starred) = ArticleStatus.STARRED.toStatusPair - - assertNull(read) - assertTrue(starred!!) - } } diff --git a/feedbinclient/src/main/java/com/jocmp/feedbinclient/mercuryparser/MercuryParser.kt b/feedbinclient/src/main/java/com/jocmp/feedbinclient/mercuryparser/MercuryParser.kt new file mode 100644 index 000000000..f59a5dee5 --- /dev/null +++ b/feedbinclient/src/main/java/com/jocmp/feedbinclient/mercuryparser/MercuryParser.kt @@ -0,0 +1,51 @@ +package com.jocmp.feedbinclient + +import com.squareup.moshi.Moshi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import okhttp3.Request +import java.util.Base64 +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec + +class MercuryParser( + private val username: String, + private val secret: String, + private val httpClient: OkHttpClient, +) { + private val adapter = ParserResultJsonAdapter(Moshi.Builder().build()) + + suspend fun parse(url: String): ParserResult? = withContext(Dispatchers.IO) { + val signature = hmacSHA1(secret, url) + val base64Url = Base64.getUrlEncoder().encodeToString(url.toByteArray()) + val request = Request.Builder() + .url("$EXTRACT_URL/parser/$username/$signature?base64_url=$base64Url") + .get() + .build() + + try { + httpClient.newCall(request).execute().use { response -> + val body = response.body?.string() + + if (response.isSuccessful && body != null) { + adapter.fromJson(body) + } else { + null + } + } + } catch (e: Exception) { + null + } + } + + private fun hmacSHA1(key: String, data: String): String { + val mac = Mac.getInstance("HmacSHA1") + mac.init(SecretKeySpec(key.toByteArray(), "HmacSHA1")) + return mac.doFinal(data.toByteArray()).joinToString("") { "%02x".format(it) } + } + + companion object { + const val EXTRACT_URL = "https://extract.feedbin.com" + } +} diff --git a/feedbinclient/src/main/java/com/jocmp/feedbinclient/mercuryparser/ParserResult.kt b/feedbinclient/src/main/java/com/jocmp/feedbinclient/mercuryparser/ParserResult.kt new file mode 100644 index 000000000..2e37ddd72 --- /dev/null +++ b/feedbinclient/src/main/java/com/jocmp/feedbinclient/mercuryparser/ParserResult.kt @@ -0,0 +1,12 @@ +package com.jocmp.feedbinclient + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ParserResult( + val title: String?, + val author: String?, + val content: String?, + val excerpt: String?, + val lead_image_url: String?, +)