├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── 1-bug-report.yml │ └── 2-full-content-request.yml └── workflows │ ├── ci.yml │ ├── create-release.yml │ └── deploy-production.yml ├── .gitignore ├── .idea ├── .name ├── AndroidProjectSystem.xml ├── codeInsightSettings.xml ├── compiler.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── kotlinc.xml ├── migrations.xml ├── misc.xml └── vcs.xml ├── .tool-versions ├── .vscode └── settings.json ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Makefile ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── google-services-debug-only.json ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── jocmp │ │ └── capyreader │ │ └── ExampleInstrumentedTest.kt │ ├── debug │ └── res │ │ └── values │ │ └── ic_launcher_background.xml │ ├── free │ └── java │ │ └── com │ │ └── capyreader │ │ └── app │ │ └── ui │ │ └── settings │ │ └── CrashReportingCheckbox.kt │ ├── gplay │ └── java │ │ └── com │ │ └── capyreader │ │ └── app │ │ └── ui │ │ └── settings │ │ └── CrashReportingCheckbox.kt │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ │ ├── com │ │ │ └── capyreader │ │ │ │ └── app │ │ │ │ ├── AccountModule.kt │ │ │ │ ├── AddFeedActivity.kt │ │ │ │ ├── ArticleStatusBroadcastReceiver.kt │ │ │ │ ├── BaseActivity.kt │ │ │ │ ├── CommonModule.kt │ │ │ │ ├── KoinSetupModules.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── MainApplication.kt │ │ │ │ ├── PreferredMaxWidth.kt │ │ │ │ ├── common │ │ │ │ ├── AccountSourceTitleExt.kt │ │ │ │ ├── AndroidDatabaseProvider.kt │ │ │ │ ├── AppFaviconFetcher.kt │ │ │ │ ├── ContextFileExt.kt │ │ │ │ ├── ContextOpenLinkExt.kt │ │ │ │ ├── ContextServiceExt.kt │ │ │ │ ├── ContextShareArticleExt.kt │ │ │ │ ├── ContextShareLinkExt.kt │ │ │ │ ├── FeedGroup.kt │ │ │ │ ├── GetContentFromMimeTypes.kt │ │ │ │ ├── ImagePreview.kt │ │ │ │ ├── MD5.kt │ │ │ │ ├── Media.kt │ │ │ │ ├── PreferenceState.kt │ │ │ │ ├── RememberVerticalGestures.kt │ │ │ │ ├── RowItem.kt │ │ │ │ ├── SharedPreferenceStoreProvider.kt │ │ │ │ ├── Toast.kt │ │ │ │ └── WebViewInterface.kt │ │ │ │ ├── logging │ │ │ │ └── CrashLogExport.kt │ │ │ │ ├── notifications │ │ │ │ ├── DeleteNotificationWorker.kt │ │ │ │ ├── NotificationHelper.kt │ │ │ │ └── Notifications.kt │ │ │ │ ├── preferences │ │ │ │ ├── AfterReadAllBehavior.kt │ │ │ │ ├── AppPreferences.kt │ │ │ │ ├── ArticleListVerticalSwipe.kt │ │ │ │ ├── ArticleVerticalSwipe.kt │ │ │ │ ├── BackAction.kt │ │ │ │ ├── LayoutPreference.kt │ │ │ │ ├── ReaderImageVisibility.kt │ │ │ │ ├── RowSwipeOption.kt │ │ │ │ └── ThemeOption.kt │ │ │ │ ├── refresher │ │ │ │ ├── FeedRefresher.kt │ │ │ │ ├── RefreshFeedsWorker.kt │ │ │ │ ├── RefreshInterval.kt │ │ │ │ ├── RefreshIntervalDurationExt.kt │ │ │ │ ├── RefreshScheduler.kt │ │ │ │ └── RefresherModule.kt │ │ │ │ ├── sync │ │ │ │ ├── ReadSyncWorker.kt │ │ │ │ ├── StarSyncWorker.kt │ │ │ │ ├── Sync.kt │ │ │ │ ├── SyncLogger.kt │ │ │ │ └── SyncModule.kt │ │ │ │ ├── transfers │ │ │ │ ├── OPMLExporter.kt │ │ │ │ └── OPMLImportWorker.kt │ │ │ │ └── ui │ │ │ │ ├── App.kt │ │ │ │ ├── ArticleStatusNavigationTitleExt.kt │ │ │ │ ├── Autofill.kt │ │ │ │ ├── CrashReporting.kt │ │ │ │ ├── LayoutHelpers.kt │ │ │ │ ├── LocalConnectivity.kt │ │ │ │ ├── PreferencesComposableExt.kt │ │ │ │ ├── Route.kt │ │ │ │ ├── accounts │ │ │ │ ├── AccountNavigation.kt │ │ │ │ ├── AccountRow.kt │ │ │ │ ├── AddAccountScreen.kt │ │ │ │ ├── AddAccountView.kt │ │ │ │ ├── AddAccountViewModel.kt │ │ │ │ ├── AuthFields.kt │ │ │ │ ├── LoginModule.kt │ │ │ │ ├── LoginScreen.kt │ │ │ │ ├── LoginView.kt │ │ │ │ ├── LoginViewModel.kt │ │ │ │ ├── MiniSettings.kt │ │ │ │ ├── ServiceSignup.kt │ │ │ │ └── UpdateLoginViewModel.kt │ │ │ │ ├── addintent │ │ │ │ └── AddFeedScreen.kt │ │ │ │ ├── articles │ │ │ │ ├── AddFeedButton.kt │ │ │ │ ├── AddFeedDialog.kt │ │ │ │ ├── AddFeedView.kt │ │ │ │ ├── AddFeedViewModel.kt │ │ │ │ ├── ArticleActions.kt │ │ │ │ ├── ArticleHandler.kt │ │ │ │ ├── ArticleList.kt │ │ │ │ ├── ArticleListBackHandler.kt │ │ │ │ ├── ArticleListFontScale.kt │ │ │ │ ├── ArticleListScaffold.kt │ │ │ │ ├── ArticleNavigation.kt │ │ │ │ ├── ArticleRow.kt │ │ │ │ ├── ArticleScaffold.kt │ │ │ │ ├── ArticleScreen.kt │ │ │ │ ├── ArticleScreenViewModel.kt │ │ │ │ ├── ArticleStatusBar.kt │ │ │ │ ├── ArticleStatusIcon.kt │ │ │ │ ├── ArticlesModule.kt │ │ │ │ ├── ColumnScrollbar.kt │ │ │ │ ├── CountBadge.kt │ │ │ │ ├── EditFolderDialog.kt │ │ │ │ ├── EditFolderView.kt │ │ │ │ ├── EditFolderViewModel.kt │ │ │ │ ├── FaviconBadge.kt │ │ │ │ ├── FeedActionMenuItems.kt │ │ │ │ ├── FilterActionMenu.kt │ │ │ │ ├── FilterAppBarTitle.kt │ │ │ │ ├── FolderActionMenuItems.kt │ │ │ │ ├── FullContentLoadingIcon.kt │ │ │ │ ├── LayoutNavigationHandler.kt │ │ │ │ ├── ListHeadline.kt │ │ │ │ ├── ListTitle.kt │ │ │ │ ├── LocalArticleLookup.kt │ │ │ │ ├── LocalFullContent.kt │ │ │ │ ├── RemoveFeedDialog.kt │ │ │ │ ├── RemoveFolderDialog.kt │ │ │ │ ├── SavedSearchRow.kt │ │ │ │ ├── UnauthorizedAlertDialog.kt │ │ │ │ ├── UpdateAuthDialog.kt │ │ │ │ ├── UpdateAuthView.kt │ │ │ │ ├── WithPositiveCount.kt │ │ │ │ ├── detail │ │ │ │ │ ├── ArticleActions.kt │ │ │ │ │ ├── ArticleBylineExt.kt │ │ │ │ │ ├── ArticleFontMenu.kt │ │ │ │ │ ├── ArticleNavigationIcon.kt │ │ │ │ │ ├── ArticlePagination.kt │ │ │ │ │ ├── ArticleReader.kt │ │ │ │ │ ├── ArticleStyleListener.kt │ │ │ │ │ ├── ArticleStylePicker.kt │ │ │ │ │ ├── ArticleTemplateColors.kt │ │ │ │ │ ├── ArticleTopBar.kt │ │ │ │ │ ├── ArticleView.kt │ │ │ │ │ ├── CapyPlaceholder.kt │ │ │ │ │ ├── CornerTapGestureScroll.kt │ │ │ │ │ ├── HorizontalReaderPager.kt │ │ │ │ │ ├── LocalMediaViewer.kt │ │ │ │ │ └── ShareLinkDialog.kt │ │ │ │ ├── feeds │ │ │ │ │ ├── FeedGroupList.kt │ │ │ │ │ ├── FeedList.kt │ │ │ │ │ ├── FeedRow.kt │ │ │ │ │ ├── FolderRow.kt │ │ │ │ │ ├── IconDropdown.kt │ │ │ │ │ ├── LocalFolderActions.kt │ │ │ │ │ ├── RefreshButtonState.kt │ │ │ │ │ └── edit │ │ │ │ │ │ ├── EditFeedDialog.kt │ │ │ │ │ │ ├── EditFeedURLDisplay.kt │ │ │ │ │ │ ├── EditFeedView.kt │ │ │ │ │ │ └── EditFeedViewModel.kt │ │ │ │ ├── list │ │ │ │ │ ├── ArticleActionMenu.kt │ │ │ │ │ ├── ArticleLayoutScrollReset.kt │ │ │ │ │ ├── ArticleListItem.kt │ │ │ │ │ ├── ArticleListTopBar.kt │ │ │ │ │ ├── ArticleRowSwipeBox.kt │ │ │ │ │ ├── ArticleRowSwipeState.kt │ │ │ │ │ ├── EmptyOnboardingView.kt │ │ │ │ │ ├── FeedActionMenu.kt │ │ │ │ │ ├── FolderActionMenu.kt │ │ │ │ │ ├── MarkAllReadButton.kt │ │ │ │ │ ├── MarkAllReadDialog.kt │ │ │ │ │ ├── MarkAllReadState.kt │ │ │ │ │ └── PullToNextFeedBox.kt │ │ │ │ └── media │ │ │ │ │ ├── ArticleMediaView.kt │ │ │ │ │ ├── CloseIconButton.kt │ │ │ │ │ ├── Colors.kt │ │ │ │ │ ├── ImageErrorView.kt │ │ │ │ │ ├── ImageSaver.kt │ │ │ │ │ ├── MediaActionButton.kt │ │ │ │ │ ├── MediaSaveButton.kt │ │ │ │ │ └── MediaShareButton.kt │ │ │ │ ├── components │ │ │ │ ├── ArticleAction.kt │ │ │ │ ├── ArticleSearch.kt │ │ │ │ ├── CopyToClipboard.kt │ │ │ │ ├── DialogCard.kt │ │ │ │ ├── DialogHorizontalDivider.kt │ │ │ │ ├── EmptyView.kt │ │ │ │ ├── FormSection.kt │ │ │ │ ├── LoadingView.kt │ │ │ │ ├── ProvideContentColorTextStyle.kt │ │ │ │ ├── SafeEdgePadding.kt │ │ │ │ ├── SearchState.kt │ │ │ │ ├── SearchTextField.kt │ │ │ │ ├── ShareLink.kt │ │ │ │ ├── Slingshot.kt │ │ │ │ ├── Spacing.kt │ │ │ │ ├── Swiper.kt │ │ │ │ ├── SwiperState.kt │ │ │ │ ├── TextSwitch.kt │ │ │ │ ├── ToolbarTooltip.kt │ │ │ │ ├── WebView.kt │ │ │ │ └── pullrefresh │ │ │ │ │ ├── CircularProgressPainter.kt │ │ │ │ │ ├── PullRefreshIndicator.kt │ │ │ │ │ └── SwipeRefresh.kt │ │ │ │ ├── fixtures │ │ │ │ ├── ArticleSample.kt │ │ │ │ ├── FeedSample.kt │ │ │ │ ├── FolderPreviewFixture.kt │ │ │ │ └── PreviewKoinApplication.kt │ │ │ │ ├── settings │ │ │ │ ├── AccountSettingsStrings.kt │ │ │ │ ├── LocalSnackbarHost.kt │ │ │ │ ├── PreferenceSelect.kt │ │ │ │ ├── SettingsList.kt │ │ │ │ ├── SettingsModule.kt │ │ │ │ ├── SettingsPanelScaffold.kt │ │ │ │ ├── SettingsScaffold.kt │ │ │ │ ├── SettingsScreen.kt │ │ │ │ ├── SettingsView.kt │ │ │ │ ├── keywordblocklist │ │ │ │ │ ├── KeywordBlocklistItem.kt │ │ │ │ │ ├── KeywordBlocklistView.kt │ │ │ │ │ └── LocalBlockedKeywords.kt │ │ │ │ └── panels │ │ │ │ │ ├── AboutSettingsPanel.kt │ │ │ │ │ ├── AccountSettingsPanel.kt │ │ │ │ │ ├── AccountSettingsViewModel.kt │ │ │ │ │ ├── ArticleListSettings.kt │ │ │ │ │ ├── AutoDeleteMenu.kt │ │ │ │ │ ├── CrashLogExportItem.kt │ │ │ │ │ ├── DisplaySettingsPanel.kt │ │ │ │ │ ├── DisplaySettingsViewModel.kt │ │ │ │ │ ├── GeneralSettingsPanel.kt │ │ │ │ │ ├── GeneralSettingsViewModel.kt │ │ │ │ │ ├── GesturesSettingsPanel.kt │ │ │ │ │ ├── GesturesSettingsViewModel.kt │ │ │ │ │ ├── NotificationCheckbox.kt │ │ │ │ │ ├── NotificationGroupCheckbox.kt │ │ │ │ │ ├── NotificationsSettingsPanel.kt │ │ │ │ │ ├── OPMLExportButton.kt │ │ │ │ │ ├── OPMLImportButton.kt │ │ │ │ │ ├── RefreshIntervalMenu.kt │ │ │ │ │ ├── SettingsPanel.kt │ │ │ │ │ ├── SettingsViewModel.kt │ │ │ │ │ ├── TestNotificationRow.kt │ │ │ │ │ └── UnreadSortOrderSelect.kt │ │ │ │ ├── theme │ │ │ │ ├── Color.kt │ │ │ │ ├── Theme.kt │ │ │ │ └── Type.kt │ │ │ │ └── widget │ │ │ │ ├── ArticleHeadline.kt │ │ │ │ ├── HeadlinesLayout.kt │ │ │ │ ├── HeadlinesWidget.kt │ │ │ │ ├── HeadlinesWidgetReceiver.kt │ │ │ │ └── WidgetUpdater.kt │ │ └── me │ │ │ └── saket │ │ │ └── swipe │ │ │ ├── ActionFinder.kt │ │ │ ├── SwipeAction.kt │ │ │ ├── SwipeRipple.kt │ │ │ ├── SwipeableActionsBox.kt │ │ │ ├── SwipeableActionsState.kt │ │ │ ├── defaults.kt │ │ │ └── horizontalDraggable.kt │ └── res │ │ ├── drawable │ │ ├── capy_icon_inline.xml │ │ ├── favicon_badge_placeholder.xml │ │ ├── feedbin_logo.xml │ │ ├── freshrss_logo.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_launcher_foreground_full_color.xml │ │ ├── ic_rounded_close.xml │ │ ├── ic_rounded_sync.xml │ │ ├── icon_add.xml │ │ ├── icon_article_empty.xml │ │ ├── icon_article_error.xml │ │ ├── icon_article_filled.xml │ │ ├── icon_article_loading.xml │ │ ├── icon_circle_filled.xml │ │ ├── icon_circle_outline.xml │ │ ├── icon_open_in_new.xml │ │ ├── icon_rounded_arrow_downward.xml │ │ ├── icon_rounded_arrow_upward.xml │ │ ├── icon_star_filled.xml │ │ ├── icon_star_outline.xml │ │ ├── newsmode.xml │ │ └── rss_logo.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── values-ar │ │ └── strings.xml │ │ ├── values-b+sr+Latn │ │ └── strings.xml │ │ ├── values-bg │ │ └── strings.xml │ │ ├── values-cs │ │ └── strings.xml │ │ ├── values-cy │ │ └── strings.xml │ │ ├── values-de │ │ └── strings.xml │ │ ├── values-el │ │ └── strings.xml │ │ ├── values-es │ │ └── strings.xml │ │ ├── values-et │ │ └── strings.xml │ │ ├── values-fr │ │ └── strings.xml │ │ ├── values-gl │ │ └── strings.xml │ │ ├── values-in │ │ └── strings.xml │ │ ├── values-it │ │ └── strings.xml │ │ ├── values-iw │ │ └── strings.xml │ │ ├── values-lv │ │ └── strings.xml │ │ ├── values-ml │ │ └── strings.xml │ │ ├── values-nb-rNO │ │ └── strings.xml │ │ ├── values-ne │ │ └── strings.xml │ │ ├── values-pl │ │ └── strings.xml │ │ ├── values-pt-rBR │ │ └── strings.xml │ │ ├── values-pt │ │ └── strings.xml │ │ ├── values-ro │ │ └── strings.xml │ │ ├── values-ru │ │ └── strings.xml │ │ ├── values-sv │ │ └── strings.xml │ │ ├── values-ta │ │ └── strings.xml │ │ ├── values-tr │ │ └── strings.xml │ │ ├── values-zh-rCN │ │ └── strings.xml │ │ ├── values-zh-rTW │ │ └── strings.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ ├── xml-v31 │ │ └── headlines_widget.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ ├── data_extraction_rules.xml │ │ ├── file_paths.xml │ │ ├── headlines_widget.xml │ │ └── locales_config.xml │ └── test │ └── java │ └── com │ ├── capyreader │ └── app │ │ └── refresher │ │ └── RefreshIntervalTest.kt │ └── jocmp │ └── capyreader │ └── .gitkeep ├── article_forge ├── .gitignore ├── .tool-versions ├── Gemfile ├── Gemfile.lock ├── Makefile ├── README.md ├── color_picker.rb ├── font_picker.rb ├── main.rb ├── public │ ├── assets │ │ ├── custom-extractors.js │ │ ├── debug.js │ │ ├── full-content.js │ │ ├── media.js │ │ └── play-arrow.svg │ └── res │ │ └── font │ │ ├── atkinson_hyperlegible.ttf │ │ ├── inter.ttf │ │ ├── jost.ttf │ │ ├── literata.ttf │ │ ├── poppins.ttf │ │ └── vollkorn.ttf ├── script │ ├── generate-android-style │ └── generate-android-template ├── style │ └── stylesheet.scss ├── style_minifier.rb └── views │ ├── article_form.liquid │ ├── image.liquid │ ├── template.liquid │ └── test-room.liquid ├── build.gradle.kts ├── bumpver.toml ├── capy ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ ├── full-content.js │ │ ├── media.js │ │ ├── mercury-parser.js │ │ ├── play-arrow.svg │ │ └── stylesheet.css │ ├── java │ │ └── com │ │ │ └── jocmp │ │ │ └── capy │ │ │ ├── Account.kt │ │ │ ├── AccountBuildArticlePagerExt.kt │ │ │ ├── AccountCountExt.kt │ │ │ ├── AccountDelegate.kt │ │ │ ├── AccountFindArticleIndexExt.kt │ │ │ ├── AccountLatestArticlesExt.kt │ │ │ ├── AccountManager.kt │ │ │ ├── AccountPreferences.kt │ │ │ ├── Article.kt │ │ │ ├── ArticleFilter.kt │ │ │ ├── ArticleNotification.kt │ │ │ ├── ArticlePages.kt │ │ │ ├── ArticleStatus.kt │ │ │ ├── Countable.kt │ │ │ ├── DatabaseProvider.kt │ │ │ ├── EditFeedFormEntry.kt │ │ │ ├── EditFolderFormEntry.kt │ │ │ ├── Enclosure.kt │ │ │ ├── Feed.kt │ │ │ ├── Folder.kt │ │ │ ├── MacroProcessor.kt │ │ │ ├── MarkRead.kt │ │ │ ├── OPMLFile.kt │ │ │ ├── PreferenceStoreProvider.kt │ │ │ ├── RandomUUID.kt │ │ │ ├── SavedSearch.kt │ │ │ ├── UserAgentInterceptor.kt │ │ │ ├── accounts │ │ │ ├── AddFeedResult.kt │ │ │ ├── AutoDelete.kt │ │ │ ├── BasicAuthInterceptor.kt │ │ │ ├── Credentials.kt │ │ │ ├── FaviconFetcher.kt │ │ │ ├── FeedOPMLExt.kt │ │ │ ├── FolderOPMLExt.kt │ │ │ ├── FreshRSSPath.kt │ │ │ ├── HttpClientBuilder.kt │ │ │ ├── LocalOkHttpClient.kt │ │ │ ├── Source.kt │ │ │ ├── SubscriptionChoice.kt │ │ │ ├── WithErrorHandling.kt │ │ │ ├── feedbin │ │ │ │ ├── FeedbinAccountDelegate.kt │ │ │ │ ├── FeedbinCredentials.kt │ │ │ │ └── FeedbinOkHttpClient.kt │ │ │ ├── local │ │ │ │ ├── ArticleURL.kt │ │ │ │ ├── LocalAccountDelegate.kt │ │ │ │ ├── ParsedItem.kt │ │ │ │ └── RichMedia.kt │ │ │ └── reader │ │ │ │ ├── BuildReaderDelegate.kt │ │ │ │ ├── InvalidFoldersError.kt │ │ │ │ ├── ReaderAccountDelegate.kt │ │ │ │ ├── ReaderCredentials.kt │ │ │ │ ├── ReaderEnclosureParsing.kt │ │ │ │ ├── ReaderOkHttpClient.kt │ │ │ │ └── TagNameExt.kt │ │ │ ├── articles │ │ │ ├── ArticleContent.kt │ │ │ ├── ArticleImageEnclosuresExt.kt │ │ │ ├── ArticleRenderer.kt │ │ │ ├── CleanLinks.kt │ │ │ ├── CleanStyles.kt │ │ │ ├── FontOption.kt │ │ │ ├── FontSize.kt │ │ │ ├── HtmlPostProcessor.kt │ │ │ ├── NextFilter.kt │ │ │ ├── ParseHTML.kt │ │ │ ├── RelativeTime.kt │ │ │ ├── RemoveImages.kt │ │ │ ├── Sort.kt │ │ │ ├── TemplateColors.kt │ │ │ ├── UnreadSortOrder.kt │ │ │ └── WrapTables.kt │ │ │ ├── common │ │ │ ├── Async.kt │ │ │ ├── CallExecuteAsyncExt.kt │ │ │ ├── CallExt.kt │ │ │ ├── CoroutineExt.kt │ │ │ ├── DataseTransactionExt.kt │ │ │ ├── DecodeHTML.kt │ │ │ ├── FeedListExt.kt │ │ │ ├── FolderListExt.kt │ │ │ ├── LocalDateTimeExt.kt │ │ │ ├── MutableListExt.kt │ │ │ ├── MutableSetExt.kt │ │ │ ├── NetworkErrorExt.kt │ │ │ ├── OptionalFile.kt │ │ │ ├── OptionalURL.kt │ │ │ ├── SavedSearchListExt.kt │ │ │ ├── StringCharactersExt.kt │ │ │ ├── StringPrependingExt.kt │ │ │ ├── StringProtocolExt.kt │ │ │ ├── SubscriptionHostExt.kt │ │ │ ├── TimeFormats.kt │ │ │ ├── TimeHelpers.kt │ │ │ ├── UnauthorizedError.kt │ │ │ ├── WindowOrigin.kt │ │ │ └── WithResult.kt │ │ │ ├── logging │ │ │ ├── CapyLog.kt │ │ │ └── Logging.kt │ │ │ ├── opml │ │ │ ├── Feed.kt │ │ │ ├── FeedOutlineExt.kt │ │ │ ├── Folder.kt │ │ │ ├── FolderOutlineExt.kt │ │ │ ├── OPMLDocument.kt │ │ │ ├── OPMLHandler.kt │ │ │ ├── OPMLImporter.kt │ │ │ └── Outline.kt │ │ │ ├── persistence │ │ │ ├── ArticleMapper.kt │ │ │ ├── ArticleNotificationMapper.kt │ │ │ ├── ArticleNotificationRecords.kt │ │ │ ├── ArticlePageMapper.kt │ │ │ ├── ArticlePagerFactory.kt │ │ │ ├── ArticleRecords.kt │ │ │ ├── ArticleStatusPair.kt │ │ │ ├── EnclosureRecords.kt │ │ │ ├── FeedRecords.kt │ │ │ ├── FolderRecords.kt │ │ │ ├── SavedSearchRecords.kt │ │ │ ├── TaggingRecords.kt │ │ │ └── articles │ │ │ │ ├── ArticleHelpers.kt │ │ │ │ ├── ByArticleStatus.kt │ │ │ │ ├── ByFeed.kt │ │ │ │ ├── BySavedSearch.kt │ │ │ │ └── NewestFirst.kt │ │ │ └── preferences │ │ │ ├── AndroidPreference.kt │ │ │ ├── AndroidPreferenceStore.kt │ │ │ ├── Preference.kt │ │ │ └── PreferenceStore.kt │ ├── res │ │ ├── font │ │ │ ├── atkinson_hyperlegible.ttf │ │ │ ├── inter.ttf │ │ │ ├── jost.ttf │ │ │ ├── literata.ttf │ │ │ ├── poppins.ttf │ │ │ └── vollkorn.ttf │ │ ├── raw │ │ │ └── template.html │ │ └── xml │ │ │ └── network_security_config.xml │ └── sqldelight │ │ └── com │ │ └── jocmp │ │ └── capy │ │ └── db │ │ ├── 10_AddDismissedAtToArticleNotifications.sqm │ │ ├── 11_CreateFolders.sqm │ │ ├── 12_CreateEnclosures.sqm │ │ ├── 1_AddFeeds.sqm │ │ ├── 2_AddArticles.sqm │ │ ├── 3_AddArticleStatuses.sqm │ │ ├── 4_AddTaggings.sqm │ │ ├── 5_AddFeedStickyContentSetting.sqm │ │ ├── 6_AddEnableNotificationsToFeeds.sqm │ │ ├── 7_AddArticleNotifications.sqm │ │ ├── 8_AddSavedSearches.sqm │ │ ├── 9_AddSavedSearchArticles.sqm │ │ ├── article_notifications.sq │ │ ├── articles.sq │ │ ├── articlesByFeed.sq │ │ ├── articlesBySavedSearch.sq │ │ ├── articlesByStatus.sq │ │ ├── enclosures.sq │ │ ├── feeds.sq │ │ ├── folders.sq │ │ ├── saved_searches.sq │ │ └── taggings.sq │ └── test │ ├── java │ └── com │ │ └── jocmp │ │ └── capy │ │ ├── AccountManagerTest.kt │ │ ├── AccountTest.kt │ │ ├── ArticleFilterTest.kt │ │ ├── InMemoryDatabaseProvider.kt │ │ ├── InMemoryPreferencesProvider.kt │ │ ├── IntExt.kt │ │ ├── MacroProcessorTest.kt │ │ ├── MockFeedFinder.kt │ │ ├── OPMLFileTest.kt │ │ ├── PathHelpers.kt │ │ ├── RandomID.kt │ │ ├── RssItemFixture.kt │ │ ├── accounts │ │ ├── ArticleContentTest.kt │ │ ├── FakeFaviconFetcher.kt │ │ ├── FreshRSSPathTest.kt │ │ ├── feedbin │ │ │ ├── FeedbinAccountDelegateTest.kt │ │ │ └── FeedbinCredentialsTest.kt │ │ ├── local │ │ │ ├── ArticleURLTest.kt │ │ │ ├── LocalAccountDelegateTest.kt │ │ │ ├── ParsedItemTest.kt │ │ │ └── RichMediaTest.kt │ │ └── reader │ │ │ ├── ItemFixtures.kt │ │ │ ├── ReaderAccountDelegateTest.kt │ │ │ ├── ReaderCredentialsTest.kt │ │ │ └── ReaderEnclosureParsingTest.kt │ │ ├── articles │ │ ├── CleanLinksTest.kt │ │ ├── HtmlHelpers.kt │ │ ├── NextFilterTest.kt │ │ ├── RelativeTimeTest.kt │ │ └── WrapTablesTest.kt │ │ ├── common │ │ ├── FeedListExtTest.kt │ │ ├── StringPrependingExtTest.kt │ │ ├── TimeHelpersTest.kt │ │ └── WindowOriginTest.kt │ │ ├── fixtures │ │ ├── AccountFixture.kt │ │ ├── ArticleFixture.kt │ │ ├── FeedFixture.kt │ │ ├── FolderFixture.kt │ │ ├── GenericFeed.kt │ │ └── SavedSearchFixture.kt │ │ ├── opml │ │ ├── OPMLHandlerTest.kt │ │ └── OPMLImporterTest.kt │ │ └── persistence │ │ ├── ArticleMapperTest.kt │ │ ├── ArticleRecordsTest.kt │ │ ├── ArticleStatusPairTest.kt │ │ ├── EnclosureRecordsTest.kt │ │ ├── FeedRecordsTest.kt │ │ ├── SavedSearchRecordsTest.kt │ │ ├── TaggingRecordsTest.kt │ │ └── articles │ │ ├── ByArticleStatusTest.kt │ │ ├── ByFeedTest.kt │ │ └── BySavedSearchTest.kt │ └── resources │ ├── article_ars_technica.html │ ├── article_substack.html │ ├── article_the_verge.html │ ├── local.xml │ ├── local_with_invalid_characters.xml │ ├── multiple_matching_feeds.xml │ └── nested_import.xml ├── debug.keystore ├── fastlane ├── Appfile ├── Fastfile ├── README.md └── metadata │ └── android │ └── en-US │ ├── changelogs │ ├── 1001.txt │ ├── 1002.txt │ ├── 1003.txt │ ├── 1004.txt │ ├── 1005.txt │ ├── 1006.txt │ ├── 1007.txt │ ├── 1009.txt │ ├── 1010.txt │ ├── 1011.txt │ ├── 1012.txt │ ├── 1013.txt │ ├── 1014.txt │ ├── 1015.txt │ ├── 1016.txt │ ├── 1017.txt │ ├── 1019.txt │ ├── 1020.txt │ ├── 1021.txt │ ├── 1022.txt │ ├── 1023.txt │ ├── 1024.txt │ ├── 1025.txt │ ├── 1026.txt │ ├── 1027.txt │ ├── 1028.txt │ ├── 1029.txt │ ├── 1030.txt │ ├── 1031.txt │ ├── 1032.txt │ ├── 1033.txt │ ├── 1034.txt │ ├── 1035.txt │ ├── 1036.txt │ ├── 1037.txt │ ├── 1038.txt │ ├── 1039.txt │ ├── 1040.txt │ ├── 1041.txt │ ├── 1042.txt │ ├── 1043.txt │ ├── 1044.txt │ ├── 1045.txt │ ├── 1046.txt │ ├── 1047.txt │ ├── 1048.txt │ ├── 1049.txt │ ├── 1050.txt │ ├── 1051.txt │ ├── 1052.txt │ ├── 1053.txt │ ├── 1054.txt │ ├── 1055.txt │ ├── 1056.txt │ ├── 1057.txt │ ├── 1058.txt │ ├── 1059.txt │ ├── 1060.txt │ ├── 1061.txt │ ├── 1062.txt │ ├── 1063.txt │ ├── 1064.txt │ ├── 1065.txt │ ├── 1066.txt │ ├── 1067.txt │ ├── 1068.txt │ ├── 1069.txt │ ├── 1070.txt │ ├── 1071.txt │ ├── 1072.txt │ ├── 1073.txt │ ├── 1074.txt │ ├── 1075.txt │ ├── 1076.txt │ ├── 1077.txt │ ├── 1078.txt │ ├── 1079.txt │ ├── 1080.txt │ ├── 1081.txt │ ├── 1082.txt │ ├── 1083.txt │ ├── 1084.txt │ ├── 1085.txt │ ├── 1086.txt │ ├── 1087.txt │ ├── 1088.txt │ ├── 1089.txt │ ├── 1090.txt │ ├── 1091.txt │ ├── 1092.txt │ ├── 1093.txt │ ├── 1094.txt │ ├── 1095.txt │ ├── 1096.txt │ ├── 1097.txt │ ├── 1098.txt │ ├── 1099.txt │ ├── 1100.txt │ ├── 1101.txt │ ├── 1102.txt │ ├── 1103.txt │ ├── 1104.txt │ ├── 1105.txt │ ├── 1106.txt │ ├── 1107.txt │ ├── 1108.txt │ ├── 1109.txt │ ├── 1110.txt │ ├── 1111.txt │ ├── 1112.txt │ ├── 1113.txt │ ├── 1114.txt │ ├── 1115.txt │ ├── 1116.txt │ ├── 1117.txt │ ├── 1118.txt │ ├── 1119.txt │ ├── 1120.txt │ ├── 1121.txt │ ├── 1122.txt │ ├── 1123.txt │ ├── 1124.txt │ ├── 1125.txt │ ├── 1126.txt │ ├── 1127.txt │ ├── 1128.txt │ ├── 1129.txt │ ├── 1130.txt │ ├── 1131.txt │ ├── 1132.txt │ ├── 1133.txt │ ├── 1134.txt │ ├── 1135.txt │ ├── 1136.txt │ ├── 1137.txt │ ├── 1138.txt │ └── 1139.txt │ ├── full_description.txt │ ├── images │ ├── featureGraphic.png │ ├── icon.png │ ├── phoneScreenshots │ │ ├── dark_article.png │ │ ├── dark_list.png │ │ ├── light_article.png │ │ └── light_list.png │ ├── sevenInchScreenshots │ │ ├── dark_article.png │ │ └── light_article.png │ └── tenInchScreenshots │ │ ├── dark_article.png │ │ ├── light_article.png │ │ └── light_list.png │ └── short_description.txt ├── feedbinclient ├── .gitignore ├── build.gradle.kts └── src │ ├── main │ └── java │ │ └── com │ │ └── jocmp │ │ └── feedbinclient │ │ ├── CreateSubscriptionRequest.kt │ │ ├── CreateTaggingRequest.kt │ │ ├── DeleteTagRequest.kt │ │ ├── Enclosure.kt │ │ ├── Entry.kt │ │ ├── ExtractedContent.kt │ │ ├── Feedbin.kt │ │ ├── Icon.kt │ │ ├── PagingInfo.kt │ │ ├── SavedSearch.kt │ │ ├── StarredEntriesRequest.kt │ │ ├── Subscription.kt │ │ ├── Tagging.kt │ │ ├── UnreadEntriesRequest.kt │ │ ├── UpdateSubscriptionRequest.kt │ │ └── UpdateTagRequest.kt │ └── test │ └── java │ └── com │ └── jocmp │ └── feedbinclient │ ├── .gitkeep │ └── PagingInfoTest.kt ├── feedfinder ├── .gitignore ├── build.gradle.kts └── src │ ├── main │ └── java │ │ └── com │ │ └── jocmp │ │ └── feedfinder │ │ ├── DefaultFeedFinder.kt │ │ ├── DefaultRequest.kt │ │ ├── FeedError.kt │ │ ├── FeedFinder.kt │ │ ├── OptionalURL.kt │ │ ├── Request.kt │ │ ├── Response.kt │ │ ├── StringProtocolExt.kt │ │ ├── parser │ │ ├── Feed.kt │ │ ├── Parser.kt │ │ └── XMLFeed.kt │ │ └── sources │ │ ├── BodyLinks.kt │ │ ├── Guess.kt │ │ ├── KnownPatterns.kt │ │ ├── MetaLinks.kt │ │ ├── ResponseDocumentExt.kt │ │ ├── Source.kt │ │ └── XML.kt │ └── test │ ├── java │ └── com │ │ └── jocmp │ │ └── feedfinder │ │ ├── Helpers.kt │ │ ├── TestRequest.kt │ │ ├── parser │ │ └── XMLFeedTest.kt │ │ └── sources │ │ ├── BodyLinksTest.kt │ │ ├── GuessTest.kt │ │ ├── KnownPatternsTest.kt │ │ ├── MetaLinksTest.kt │ │ └── XMLTest.kt │ └── resources │ ├── arstechnica.html │ ├── arstechnica_feed.xml │ ├── brasildefato_com_br.xml │ ├── feed.xml │ ├── microsoft_support.xml │ ├── test_index.html │ ├── theverge.html │ └── theverge_feed.xml ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── readerclient ├── .gitignore ├── build.gradle.kts └── src │ ├── main │ └── java │ │ └── com │ │ └── jocmp │ │ └── readerclient │ │ ├── Category.kt │ │ ├── GoogleReader.kt │ │ ├── Item.kt │ │ ├── ItemIdentifiers.kt │ │ ├── ItemRef.kt │ │ ├── Stream.kt │ │ ├── StreamContentsResult.kt │ │ ├── StreamItemIDsResult.kt │ │ ├── StreamItemsContentsResult.kt │ │ ├── Subscription.kt │ │ ├── SubscriptionEditAction.kt │ │ ├── SubscriptionListResult.kt │ │ ├── SubscriptionQuickAddResult.kt │ │ ├── Tag.kt │ │ ├── TagListResult.kt │ │ └── ext │ │ └── GoogleReaderExt.kt │ └── test │ └── kotlin │ └── com │ └── jocmp │ └── readerclient │ └── ItemTest.kt ├── rssparser ├── .gitignore ├── build.gradle.kts └── src │ ├── main │ └── kotlin │ │ └── com │ │ └── jocmp │ │ └── rssparser │ │ ├── RssParser.kt │ │ ├── RssParserBuilder.kt │ │ ├── exception │ │ ├── HttpException.kt │ │ └── RssParsingException.kt │ │ ├── internal │ │ ├── ChannelFactory.kt │ │ ├── DefaultFetcher.kt │ │ ├── DefaultParser.kt │ │ ├── FeedHandler.kt │ │ ├── Fetcher.kt │ │ ├── ImagePolicy.kt │ │ ├── Parser.kt │ │ ├── ParserInput.kt │ │ ├── atom │ │ │ ├── AtomFeedHandler.kt │ │ │ └── AtomKeyword.kt │ │ ├── json │ │ │ ├── JsonFeedHandler.kt │ │ │ └── models │ │ │ │ ├── Author.kt │ │ │ │ ├── Feed.kt │ │ │ │ ├── Hub.kt │ │ │ │ └── Item.kt │ │ ├── rdf │ │ │ ├── RdfFeedHandler.kt │ │ │ └── RdfKeyword.kt │ │ └── rss │ │ │ ├── RssFeedHandler.kt │ │ │ └── RssKeyword.kt │ │ └── model │ │ ├── ItunesData.kt │ │ ├── Media.kt │ │ ├── RssChannel.kt │ │ ├── RssImage.kt │ │ ├── RssItem.kt │ │ └── RssItemEnclosure.kt │ └── test │ ├── kotlin │ └── com │ │ └── jocmp │ │ └── rssparser │ │ ├── BaseParserTest.kt │ │ ├── MalformedFeedParserTest.kt │ │ ├── ParserFactory.kt │ │ ├── TestUtils.kt │ │ ├── atom │ │ ├── XmlParserAtomCategoryAttributeTest.kt │ │ ├── XmlParserAtomContentHtmlTest.kt │ │ ├── XmlParserAtomExampleTest.kt │ │ ├── XmlParserAtomFeedContentTest.kt │ │ ├── XmlParserAtomFeedCreatedUpdated.kt │ │ ├── XmlParserAtomImageQueryParams.kt │ │ ├── XmlParserAtomRepliesLink.kt │ │ ├── XmlParserAtomSelfLink.kt │ │ ├── XmlParserAtomTest.kt │ │ └── XmlParserAtomYouTube.kt │ │ ├── json │ │ ├── JsonKibtyTownTest.kt │ │ └── JsonNprTest.kt │ │ ├── rdf │ │ ├── XmlParserRdfDistroWatchTest.kt │ │ └── XmlParserRdfTest.kt │ │ └── rss │ │ ├── XmlParserAccentsTest.kt │ │ ├── XmlParserAudioFeedTest.kt │ │ ├── XmlParserBingFeedImage.kt │ │ ├── XmlParserCharEscape.kt │ │ ├── XmlParserCharsetFeedTest.kt │ │ ├── XmlParserCommentsTest.kt │ │ ├── XmlParserFeedRuTest.kt │ │ ├── XmlParserFeedThumbTest.kt │ │ ├── XmlParserGreekTest.kt │ │ ├── XmlParserHindiChannelImageTest.kt │ │ ├── XmlParserImage2FeedTest.kt │ │ ├── XmlParserImageChannelReverseTest.kt │ │ ├── XmlParserImageEmptyTag.kt │ │ ├── XmlParserImageEnclosure.kt │ │ ├── XmlParserImageFeedTest.kt │ │ ├── XmlParserImageLinkTest.kt │ │ ├── XmlParserItemAuthorTest.kt │ │ ├── XmlParserItemChannelImageTest.kt │ │ ├── XmlParserItunesFeedTest.kt │ │ ├── XmlParserItunesSeasonFeedTest.kt │ │ ├── XmlParserMultipleImageAndVideo.kt │ │ ├── XmlParserMultipleMediaContentFeedTest.kt │ │ ├── XmlParserSourceTest.kt │ │ ├── XmlParserStandardFeedTest.kt │ │ ├── XmlParserTimeFeedTest.kt │ │ ├── XmlParserUnexpectedTokenTest.kt │ │ ├── XmlParserXSLFeedTest.kt │ │ └── XmlPreventRepeatedImagesTest.kt │ └── resources │ ├── atom-feed-content.xml │ ├── atom-feed-created-updated.xml │ ├── atom-feed-youtube.xml │ ├── atom-image-query-params.xml │ ├── atom-replies-link-example.xml │ ├── atom-self-link-example.xml │ ├── atom-test-example.xml │ ├── feed-atom-test.xml │ ├── feed-bing-image.xml │ ├── feed-char-escape.xml │ ├── feed-comment.xml │ ├── feed-image-enclosure.xml │ ├── feed-item-author.xml │ ├── feed-item-channel-image.xml │ ├── feed-itunes-season.xml │ ├── feed-itunes.xml │ ├── feed-kibty-town.json │ ├── feed-multiple-media-content.xml │ ├── feed-npr-world.json │ ├── feed-prevent-repeated-images.xml │ ├── feed-rdf-distrowatch.xml │ ├── feed-rdf-test.xml │ ├── feed-test-accents.xml │ ├── feed-test-atom-category-attribute.xml │ ├── feed-test-atom-content-html.xml │ ├── feed-test-audio.xml │ ├── feed-test-charset.xml │ ├── feed-test-greek.xml │ ├── feed-test-hindi-channel-image.xml │ ├── feed-test-image-2.xml │ ├── feed-test-image-channel-reverse.xml │ ├── feed-test-image-empty-tag.xml │ ├── feed-test-image-link.xml │ ├── feed-test-image.xml │ ├── feed-test-malformed.xml │ ├── feed-test-multiple-image-and-video.xml │ ├── feed-test-ru.xml │ ├── feed-test-source.xml │ ├── feed-test-thumb.xml │ ├── feed-test-time.xml │ ├── feed-test-unexpected-token.xml │ ├── feed-test-xsl.xml │ └── feed-test.xml ├── scripts ├── base64_encode └── changelog ├── settings.gradle.kts ├── site ├── capy.png └── play-badge.png └── technotes ├── AuthFlow.md ├── FeedbinAPI.md ├── Fonts.md ├── ImporterExporter.md ├── Models.md ├── Notifications.md ├── Parsing.md ├── Persistence.md ├── Refresh.md ├── Scratch ├── 2024-08 Media.md └── 2024-11 Mercury Parser.md ├── Sync.md └── ui.md /.gitattributes: -------------------------------------------------------------------------------- 1 | article_forge/articles/fixtures/**/* linguist-generated=true 2 | article_forge/public/**/* linguist-generated=true 3 | capy/src/main/assets/*.css linguist-generated=true 4 | capy/src/main/assets/*.js linguist-generated=true 5 | capy/src/main/res/raw/*.html linguist-generated=true 6 | capy/src/test/resources/*.html linguist-generated=true 7 | feedfinder/src/test/resources/*.html linguist-generated=true 8 | feedfinder/src/test/resources/*.xml linguist-generated=true 9 | rssparser/src/test/resources/*.html linguist-generated=true 10 | rssparser/src/test/resources/*.xml linguist-generated=true 11 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: capyreader 2 | github: jocmp 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: push 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4.2.1 10 | - name: Set up Java 11 | uses: actions/setup-java@v4.4.0 12 | with: 13 | distribution: 'zulu' 14 | java-version: "21" 15 | - name: Set up Ruby 16 | uses: ruby/setup-ruby@v1 17 | with: 18 | bundler-cache: true 19 | - name: Run tests 20 | run: make test 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | /.idea/deploymentTargetDropDown.xml 11 | .idea/deploymentTargetSelector.xml 12 | .idea/codeStyles/* 13 | .idea/other.xml 14 | .idea/androidTestResultsUserPreferences.xml 15 | 16 | .DS_Store 17 | /build 18 | /captures 19 | .externalNativeBuild 20 | .cxx 21 | local.properties 22 | fastlane/report.xml 23 | app/release/ 24 | app/google-services.json 25 | 26 | # Secrets 27 | google-play-service-account.json 28 | app/google-services.json 29 | secrets.properties 30 | release.keystore 31 | 32 | # Build 33 | vendor/ 34 | app/gplay/release/ 35 | 36 | # Cruft 37 | technotes/.obsidian/ 38 | .idea/runConfigurations.xml 39 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | Capy Reader -------------------------------------------------------------------------------- /.idea/AndroidProjectSystem.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/codeInsightSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | org.junit 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | ruby 3.4.1 2 | python 3.12.4 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "yaml.schemas": { 3 | "https://json.schemastore.org/github-issue-forms.json": "file:///Users/jocmp/dev/capyreader/.github/ISSUE_TEMPLATE/*.yml" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem 'fastlane', "~> 2.226.0" 4 | gem "rexml", "~> 3.4.0" 5 | 6 | 7 | # Deps for fastlane 8 | # See https://github.com/fastlane/fastlane/issues/21942 9 | gem 'abbrev' 10 | gem 'logger' 11 | gem 'mutex_m' 12 | gem 'csv' 13 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/src/debug/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #489FB5 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jocmp/capyreader/699c32b80716b72a4fdf1dd650cf9f3e7e10f001/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/PreferredMaxWidth.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app 2 | 3 | import androidx.compose.foundation.layout.fillMaxWidth 4 | import androidx.compose.foundation.layout.widthIn 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.Stable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.unit.dp 9 | import com.capyreader.app.ui.isCompact 10 | 11 | @Stable 12 | fun Modifier.widthMaxSingleColumn() = then(Modifier.widthIn(max = 450.dp)) 13 | 14 | @Composable 15 | fun Modifier.preferredMaxWidth() = then( 16 | if (isCompact()) { 17 | Modifier.fillMaxWidth() 18 | } else { 19 | Modifier.widthIn(max = 600.dp) 20 | } 21 | ) 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/common/AccountSourceTitleExt.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.common 2 | 3 | import com.capyreader.app.R 4 | import com.jocmp.capy.accounts.Source 5 | 6 | val Source.titleKey: Int 7 | get() = when (this) { 8 | Source.FEEDBIN -> R.string.account_source_feedbin 9 | Source.FRESHRSS -> R.string.account_source_freshrss 10 | Source.LOCAL -> R.string.account_source_local 11 | Source.READER -> R.string.account_source_reader 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/common/AppFaviconFetcher.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.common 2 | 3 | import android.content.Context 4 | import coil.imageLoader 5 | import coil.request.ImageRequest 6 | import com.jocmp.capy.accounts.FaviconFetcher 7 | 8 | class AppFaviconFetcher(private val context: Context) : FaviconFetcher { 9 | override suspend fun isValid(url: String?): Boolean { 10 | url ?: return false 11 | 12 | val result = context.imageLoader 13 | .execute( 14 | ImageRequest.Builder(context) 15 | .data(url) 16 | .build() 17 | ) 18 | 19 | return result.drawable != null 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/common/ContextServiceExt.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.common 2 | 3 | import android.app.NotificationManager 4 | import android.content.Context 5 | 6 | val Context.notificationManager 7 | get() = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/common/ContextShareArticleExt.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.common 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import com.jocmp.capy.Article 6 | 7 | fun Context.shareArticle(article: Article) { 8 | val url = article.url ?: return 9 | 10 | val share = Intent.createChooser(Intent().apply { 11 | type = "text/plain" 12 | action = Intent.ACTION_SEND 13 | putExtra(Intent.EXTRA_TEXT, url.toString()) 14 | putExtra(Intent.EXTRA_TITLE, article.title) 15 | }, null) 16 | startActivity(share) 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/common/ContextShareLinkExt.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.common 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | 6 | fun Context.shareLink(url: String, title: String) { 7 | val share = Intent.createChooser(Intent().apply { 8 | type = "text/plain" 9 | action = Intent.ACTION_SEND 10 | putExtra(Intent.EXTRA_TEXT, url) 11 | putExtra(Intent.EXTRA_TITLE, title) 12 | }, null) 13 | startActivity(share) 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/common/FeedGroup.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.common 2 | 3 | 4 | enum class FeedGroup { 5 | FEEDS, 6 | FOLDERS, 7 | SAVED_SEARCHES, 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/common/MD5.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.common 2 | 3 | import java.security.MessageDigest 4 | 5 | @OptIn(ExperimentalStdlibApi::class) 6 | object MD5 { 7 | fun from(value: String): String { 8 | val md = MessageDigest.getInstance("MD5") 9 | val digest = md.digest(value.toByteArray()) 10 | return digest.toHexString() 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/common/Media.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.common 2 | 3 | import androidx.compose.runtime.MutableState 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.saveable.Saver 6 | import kotlinx.serialization.Serializable 7 | import kotlinx.serialization.encodeToString 8 | import kotlinx.serialization.json.Json 9 | 10 | @Serializable 11 | data class Media( 12 | val url: String, 13 | val altText: String? 14 | ) 15 | 16 | val Media.Companion.Saver 17 | get() = Saver, String>( 18 | save = { state -> 19 | Json.encodeToString(state.value) 20 | }, 21 | restore = { jsonString -> 22 | mutableStateOf(Json.decodeFromString(jsonString)) 23 | } 24 | ) 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/common/PreferenceState.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.common 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.State 5 | import androidx.compose.runtime.collectAsState 6 | import androidx.compose.runtime.rememberCoroutineScope 7 | import com.jocmp.capy.preferences.Preference 8 | 9 | @Composable 10 | fun Preference.asState(): State { 11 | val scope = rememberCoroutineScope() 12 | 13 | return this 14 | .stateIn(scope) 15 | .collectAsState() 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/common/RememberVerticalGestures.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.common 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.State 5 | import com.capyreader.app.preferences.AppPreferences 6 | import com.capyreader.app.ui.collectChangesWithCurrent 7 | import org.koin.compose.koinInject 8 | 9 | @Composable 10 | fun rememberTalkbackPreference(appPreferences: AppPreferences = koinInject()): State { 11 | return appPreferences.readerOptions.improveTalkback.collectChangesWithCurrent() 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/common/RowItem.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.common 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.unit.dp 8 | 9 | @Composable 10 | fun RowItem( 11 | content: @Composable () -> Unit 12 | ) { 13 | Column(Modifier.padding(horizontal = 16.dp)) { 14 | content() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/notifications/Notifications.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.notifications 2 | 3 | import android.os.Build 4 | 5 | enum class Notifications(val channelID: String) { 6 | OPML_IMPORT(channelID = "opml_import"), 7 | 8 | FEED_UPDATE(channelID = "feed_update"); 9 | 10 | companion object { 11 | val askForPermission = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU 12 | 13 | const val OPML_IMPORT_NOTIFICATION_ID = 6_170_000 14 | 15 | const val FEED_UPDATE_GROUP_NOTIFICATION_ID = 6_170_100 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/preferences/ArticleListVerticalSwipe.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.preferences 2 | 3 | import com.capyreader.app.R 4 | 5 | enum class ArticleListVerticalSwipe { 6 | DISABLED, 7 | NEXT_FEED; 8 | 9 | val translationKey: Int 10 | get() = when (this) { 11 | DISABLED -> R.string.article_list_swipe_disabled 12 | NEXT_FEED -> R.string.article_list_swipe_next_feed 13 | } 14 | 15 | companion object { 16 | val default = NEXT_FEED 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/preferences/BackAction.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.preferences 2 | 3 | import com.capyreader.app.R 4 | 5 | enum class BackAction { 6 | SYSTEM_BACK, 7 | OPEN_DRAWER, 8 | NAVIGATE_TO_PARENT; 9 | 10 | val translationKey: Int 11 | get() = when (this) { 12 | SYSTEM_BACK -> R.string.settings_gestures_list_back_navigation_system_back 13 | OPEN_DRAWER -> R.string.settings_gestures_list_back_navigation_open_drawer 14 | NAVIGATE_TO_PARENT -> R.string.settings_gestures_list_back_navigation_navigate_to_parent 15 | } 16 | 17 | companion object { 18 | val default = SYSTEM_BACK 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/preferences/LayoutPreference.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.preferences 2 | 3 | import com.capyreader.app.R 4 | 5 | enum class LayoutPreference { 6 | RESPONSIVE, 7 | SINGLE; 8 | 9 | val translationKey: Int 10 | get() = when(this) { 11 | RESPONSIVE -> R.string.layout_preference_responsive 12 | SINGLE -> R.string.layout_preference_single 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/preferences/ReaderImageVisibility.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.preferences 2 | 3 | import com.capyreader.app.R 4 | 5 | enum class ReaderImageVisibility { 6 | ALWAYS_SHOW, 7 | ALWAYS_HIDE, 8 | SHOW_ON_WIFI; 9 | 10 | val translationKey: Int 11 | get() = when (this) { 12 | ALWAYS_SHOW -> R.string.reader_image_visibility_always_show 13 | ALWAYS_HIDE -> R.string.reader_image_visibility_always_hide 14 | SHOW_ON_WIFI ->R.string.reader_image_visibility_show_on_wifi 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/preferences/ThemeOption.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.preferences 2 | 3 | import com.capyreader.app.R 4 | 5 | enum class ThemeOption { 6 | LIGHT, 7 | DARK, 8 | SYSTEM_DEFAULT; 9 | 10 | val translationKey: Int 11 | get() = when(this) { 12 | LIGHT -> R.string.theme_menu_option_light 13 | DARK -> R.string.theme_menu_option_dark 14 | SYSTEM_DEFAULT -> R.string.theme_menu_option_system_default 15 | } 16 | 17 | companion object { 18 | val default = SYSTEM_DEFAULT 19 | 20 | val sorted: List 21 | get() = listOf( 22 | SYSTEM_DEFAULT, 23 | LIGHT, 24 | DARK, 25 | ) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/refresher/RefreshFeedsWorker.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.refresher 2 | 3 | import android.content.Context 4 | import androidx.work.CoroutineWorker 5 | import androidx.work.WorkerParameters 6 | import org.koin.core.component.KoinComponent 7 | import org.koin.core.component.inject 8 | 9 | class RefreshFeedsWorker( 10 | appContext: Context, 11 | workerParams: WorkerParameters 12 | ) : CoroutineWorker(appContext, workerParams), KoinComponent { 13 | private val refresher by inject() 14 | 15 | override suspend fun doWork(): Result { 16 | return try { 17 | refresher.refresh() 18 | Result.success() 19 | } catch (e: Exception) { 20 | Result.failure() 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/refresher/RefreshInterval.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.refresher 2 | 3 | enum class RefreshInterval { 4 | MANUALLY_ONLY, 5 | ON_START, 6 | EVERY_FIFTEEN_MINUTES, 7 | EVERY_THIRTY_MINUTES, 8 | EVERY_HOUR, 9 | EVERY_12_HOURS, 10 | EVERY_DAY; 11 | 12 | val isPeriodic: Boolean 13 | get() = !(this == MANUALLY_ONLY || this == ON_START) 14 | 15 | companion object { 16 | val default = ON_START 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/refresher/RefreshIntervalDurationExt.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.refresher 2 | 3 | import com.capyreader.app.refresher.RefreshInterval.* 4 | import java.util.concurrent.TimeUnit 5 | 6 | val RefreshInterval.toTime: Pair? 7 | get() = when (this) { 8 | EVERY_FIFTEEN_MINUTES -> Pair(15, TimeUnit.MINUTES) 9 | EVERY_THIRTY_MINUTES -> Pair(30, TimeUnit.MINUTES) 10 | EVERY_HOUR -> Pair(1, TimeUnit.HOURS) 11 | EVERY_12_HOURS -> Pair(12, TimeUnit.HOURS) 12 | EVERY_DAY -> Pair(24, TimeUnit.HOURS) 13 | ON_START -> null 14 | MANUALLY_ONLY -> null 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/refresher/RefresherModule.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.refresher 2 | 3 | import org.koin.androidx.workmanager.dsl.worker 4 | import org.koin.dsl.module 5 | 6 | val refresherModule = module { 7 | single { FeedRefresher(account = get(), get(), get()) } 8 | single { RefreshScheduler(get(), get()) } 9 | worker { RefreshFeedsWorker(get(), get()) } 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/sync/SyncLogger.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.sync 2 | 3 | import com.jocmp.capy.logging.CapyLog 4 | 5 | internal object SyncLogger { 6 | fun logError( 7 | error: Throwable, 8 | value: Boolean, 9 | workType: String, 10 | articleIDs: List 11 | ) { 12 | CapyLog.error( 13 | "sync_work", 14 | error, 15 | mapOf( 16 | "work_type" to workType, 17 | "work_value" to value.toString(), 18 | "size" to articleIDs.size.toString() 19 | ) 20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/sync/SyncModule.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.sync 2 | 3 | import org.koin.androidx.workmanager.dsl.worker 4 | import org.koin.dsl.module 5 | 6 | val syncModule = module { 7 | worker { ReadSyncWorker(get(), get()) } 8 | worker { StarSyncWorker(get(), get()) } 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/ui/ArticleStatusNavigationTitleExt.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.ui 2 | 3 | import com.jocmp.capy.ArticleStatus 4 | import com.capyreader.app.R 5 | 6 | val ArticleStatus.navigationTitle: Int 7 | get() = when (this) { 8 | ArticleStatus.ALL -> R.string.filter_all 9 | ArticleStatus.UNREAD -> R.string.filter_unread 10 | ArticleStatus.STARRED -> R.string.filter_starred 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/ui/CrashReporting.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.ui 2 | 3 | import com.capyreader.app.BuildConfig 4 | 5 | object CrashReporting { 6 | const val isAvailable = BuildConfig.FLAVOR == "gplay" 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/ui/PreferencesComposableExt.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.ui 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.State 5 | import androidx.compose.runtime.collectAsState 6 | import com.jocmp.capy.preferences.Preference 7 | 8 | @Composable 9 | fun Preference.collectChangesWithDefault(initial: T = defaultValue()): State = 10 | changes().collectAsState(initial = initial) 11 | 12 | @Composable 13 | fun Preference.collectChangesWithCurrent(initial: T = get()): State = 14 | changes().collectAsState(initial = initial) 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/ui/Route.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.ui 2 | 3 | import com.jocmp.capy.accounts.Source 4 | import kotlinx.serialization.Serializable 5 | 6 | sealed class Route { 7 | @Serializable 8 | data object AddAccount : Route() 9 | 10 | @Serializable 11 | data class Login(val source: Source) : Route() 12 | 13 | @Serializable 14 | data object Settings : Route() 15 | 16 | @Serializable 17 | data object Articles : Route() 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/ui/accounts/AddAccountScreen.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.ui.accounts 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.jocmp.capy.accounts.Source 5 | import org.koin.compose.koinInject 6 | 7 | @Composable 8 | fun AddAccountScreen( 9 | onAddSuccess: () -> Unit, 10 | onNavigateToLogin: (source: Source) -> Unit, 11 | viewModel: AddAccountViewModel = koinInject() 12 | ) { 13 | AddAccountView( 14 | onSelectLocal = { 15 | viewModel.addLocalAccount() 16 | onAddSuccess() 17 | }, 18 | onSelectService = onNavigateToLogin 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/ui/accounts/AddAccountViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.ui.accounts 2 | 3 | import androidx.lifecycle.ViewModel 4 | import com.jocmp.capy.AccountManager 5 | import com.jocmp.capy.accounts.Source 6 | import com.capyreader.app.preferences.AppPreferences 7 | import com.capyreader.app.loadAccountModules 8 | 9 | class AddAccountViewModel( 10 | private val accountManager: AccountManager, 11 | private val appPreferences: AppPreferences, 12 | ) : ViewModel() { 13 | fun addLocalAccount() { 14 | val accountID = accountManager.createAccount(source = Source.LOCAL) 15 | 16 | selectAccount(accountID) 17 | 18 | loadAccountModules() 19 | } 20 | 21 | private fun selectAccount(id: String) { 22 | appPreferences.accountID.set(id) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/ui/accounts/LoginModule.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.ui.accounts 2 | 3 | import org.koin.androidx.viewmodel.dsl.viewModel 4 | import org.koin.dsl.module 5 | 6 | val loginModule = module { 7 | viewModel { 8 | AddAccountViewModel( 9 | accountManager = get(), 10 | appPreferences = get() 11 | ) 12 | } 13 | viewModel { 14 | LoginViewModel( 15 | handle = get(), 16 | accountManager = get(), 17 | appPreferences = get() 18 | ) 19 | } 20 | viewModel { 21 | UpdateLoginViewModel( 22 | account = get() 23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/ui/articles/ArticleActions.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.ui.articles 2 | 3 | import androidx.compose.runtime.Stable 4 | import androidx.compose.runtime.compositionLocalOf 5 | 6 | val LocalArticleActions = compositionLocalOf { ArticleActions() } 7 | 8 | @Stable 9 | data class ArticleActions( 10 | val markRead: (articleID: String) -> Unit = {}, 11 | val star: (articleID: String) -> Unit = {}, 12 | val markUnread: (articleID: String) -> Unit = {}, 13 | val unstar: (articleID: String) -> Unit = {}, 14 | ) 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/ui/articles/ArticleNavigation.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.ui.articles 2 | 3 | import androidx.navigation.NavController 4 | import androidx.navigation.NavGraphBuilder 5 | import androidx.navigation.compose.composable 6 | import com.capyreader.app.ui.Route 7 | 8 | fun NavGraphBuilder.articleGraph( 9 | navController: NavController, 10 | ) { 11 | composable { 12 | ArticleScreen( 13 | onNavigateToSettings = { 14 | navController.navigate(Route.Settings) { 15 | launchSingleTop = true 16 | } 17 | } 18 | ) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/ui/articles/CountBadge.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.ui.articles 2 | 3 | import androidx.compose.material3.Text 4 | import androidx.compose.runtime.Composable 5 | 6 | @Composable 7 | fun CountBadge(count: Long) { 8 | if (count < 1) { 9 | return 10 | } 11 | 12 | Text(count.toString()) 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/ui/articles/LayoutNavigationHandler.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.ui.articles 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.LaunchedEffect 5 | import com.capyreader.app.ui.rememberLayoutPreference 6 | 7 | @Composable 8 | fun LayoutNavigationHandler( 9 | enabled: Boolean, 10 | onChange: suspend () -> Unit, 11 | ) { 12 | val layout = rememberLayoutPreference() 13 | 14 | LaunchedEffect(layout) { 15 | if (enabled) { 16 | onChange() 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/ui/articles/ListHeadline.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.ui.articles 2 | 3 | import androidx.compose.foundation.layout.padding 4 | import androidx.compose.material3.MaterialTheme 5 | import androidx.compose.material3.Text 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.tooling.preview.Preview 9 | import androidx.compose.ui.unit.dp 10 | 11 | @Composable 12 | fun ListHeadline(text: String) { 13 | Text( 14 | text, 15 | style = MaterialTheme.typography.titleSmall, 16 | modifier = Modifier.padding(16.dp) 17 | ) 18 | } 19 | 20 | @Preview 21 | @Composable 22 | fun ListHeadlinePreview() { 23 | ListHeadline(text = "Ars Technica") 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/ui/articles/ListTitle.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.ui.articles 2 | 3 | import androidx.compose.material3.Text 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.text.style.TextOverflow 6 | 7 | @Composable 8 | fun ListTitle(text: String) { 9 | Text( 10 | text, 11 | overflow = TextOverflow.Ellipsis, 12 | maxLines = 1, 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/ui/articles/LocalArticleLookup.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.ui.articles 2 | 3 | import androidx.compose.runtime.compositionLocalOf 4 | import com.jocmp.capy.ArticlePages 5 | import kotlinx.coroutines.flow.Flow 6 | import kotlinx.coroutines.flow.emptyFlow 7 | 8 | val LocalArticleLookup = compositionLocalOf { ArticleLookup() } 9 | 10 | data class ArticleLookup( 11 | val findArticlePages: (articleID: String) -> Flow = { emptyFlow() } 12 | ) 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/ui/articles/LocalFullContent.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.ui.articles 2 | 3 | import androidx.compose.runtime.compositionLocalOf 4 | 5 | val LocalFullContent = compositionLocalOf { FullContentFetcher() } 6 | 7 | data class FullContentFetcher( 8 | val fetch: () -> Unit = {}, 9 | val reset: () -> Unit = {} 10 | ) 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/ui/articles/WithPositiveCount.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.ui.articles 2 | 3 | import com.jocmp.capy.ArticleStatus 4 | import com.jocmp.capy.Countable 5 | 6 | fun List.withPositiveCount(status: ArticleStatus): List { 7 | return filter { status == ArticleStatus.ALL || it.count > 0 } 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/ui/articles/detail/LocalMediaViewer.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.ui.articles.detail 2 | 3 | import androidx.compose.runtime.compositionLocalOf 4 | 5 | val LocalMediaViewer = compositionLocalOf { MediaViewer() } 6 | 7 | data class MediaViewer( 8 | val open: (url: String) -> Unit = {} 9 | ) 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/ui/articles/feeds/LocalFolderActions.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.ui.articles.feeds 2 | 3 | import androidx.compose.runtime.compositionLocalOf 4 | 5 | val LocalFolderActions = compositionLocalOf { FolderActions() } 6 | 7 | data class FolderActions( 8 | val updateExpanded: (folderName: String, expanded: Boolean) -> Unit = { _, _ -> }, 9 | ) 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/ui/articles/list/MarkAllReadState.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.ui.articles.list 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.State 5 | import com.capyreader.app.preferences.AppPreferences 6 | import com.capyreader.app.common.asState 7 | import org.koin.compose.koinInject 8 | 9 | @Composable 10 | internal fun rememberMarkAllReadState( 11 | appPreferences: AppPreferences = koinInject(), 12 | ): State { 13 | return appPreferences.articleListOptions.confirmMarkAllRead.asState() 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/ui/articles/media/Colors.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.ui.articles.media 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | object MediaColors { 6 | val textColor = Color.White.copy(0.8f) 7 | 8 | val buttonContentColor = Color.White 9 | 10 | val buttonOutlineColor = Color.White 11 | 12 | val buttonContainerColor = Color.Transparent 13 | } 14 | 15 | 16 | const val ListItemDisabledLabelTextOpacity = 0.38f 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/ui/components/ArticleSearch.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.ui.components 2 | 3 | data class ArticleSearch( 4 | val query: String? = null, 5 | val start: () -> Unit = {}, 6 | val clear: () -> Unit = {}, 7 | val update: (query: String) -> Unit = {}, 8 | val state: SearchState = SearchState.INACTIVE, 9 | ) { 10 | val isActive 11 | get() = state == SearchState.ACTIVE 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/ui/components/CopyToClipboard.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.ui.components 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.platform.LocalClipboardManager 5 | import androidx.compose.ui.text.AnnotatedString 6 | 7 | @Composable 8 | fun buildCopyToClipboard(text: String): () -> Unit { 9 | val clipboardManager = LocalClipboardManager.current 10 | 11 | return { 12 | clipboardManager.setText(AnnotatedString(text)) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/ui/components/DialogCard.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.ui.components 2 | 3 | import androidx.compose.foundation.layout.sizeIn 4 | import androidx.compose.material3.Card 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.unit.dp 8 | 9 | @Composable 10 | fun DialogCard(content: @Composable () -> Unit) { 11 | Card( 12 | Modifier.sizeIn(maxHeight = 600.dp, maxWidth = 360.dp) 13 | ) { 14 | content() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/ui/components/DialogHorizontalDivider.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.ui.components 2 | 3 | import androidx.compose.material3.HorizontalDivider 4 | import androidx.compose.material3.MaterialTheme.colorScheme 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | 8 | @Composable 9 | fun DialogHorizontalDivider(modifier: Modifier = Modifier) { 10 | HorizontalDivider( 11 | color = colorScheme.onSurfaceVariant, 12 | modifier = modifier, 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/ui/components/SearchState.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.ui.components 2 | 3 | enum class SearchState { 4 | ACTIVE, 5 | INACTIVE, 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/ui/components/Spacing.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.ui.components 2 | 3 | import androidx.compose.ui.unit.dp 4 | 5 | object Spacing { 6 | val topBarHeight = 56.dp 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/ui/settings/LocalSnackbarHost.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.ui.settings 2 | 3 | import androidx.compose.material3.SnackbarHostState 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.compositionLocalOf 6 | import androidx.compose.runtime.rememberCoroutineScope 7 | import com.jocmp.capy.common.launchUI 8 | 9 | val LocalSnackbarHost = compositionLocalOf { SnackbarHostState() } 10 | 11 | @Composable 12 | fun localSnackbarDisplay(): (message: String) -> Unit { 13 | val snackbar = LocalSnackbarHost.current 14 | val scope = rememberCoroutineScope() 15 | 16 | return { message: String -> 17 | scope.launchUI { 18 | snackbar.showSnackbar(message) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/ui/settings/SettingsScreen.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.ui.settings 2 | 3 | import androidx.compose.runtime.Composable 4 | 5 | @Composable 6 | fun SettingsScreen( 7 | onRemoveAccount: () -> Unit, 8 | onNavigateBack: () -> Unit, 9 | ) { 10 | SettingsView( 11 | onNavigateBack = onNavigateBack, 12 | onRemoveAccount = onRemoveAccount, 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/ui/settings/keywordblocklist/LocalBlockedKeywords.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.ui.settings.keywordblocklist 2 | 3 | import androidx.compose.runtime.compositionLocalOf 4 | 5 | val LocalBlockedKeywords = compositionLocalOf { BlockedKeywords() } 6 | 7 | data class BlockedKeywords( 8 | val add: (keyword: String) -> Unit = {}, 9 | val remove: (keyword: String) -> Unit = {}, 10 | val keywords: List = emptyList(), 11 | ) 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/ui/settings/panels/SettingsViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.ui.settings.panels 2 | 3 | import androidx.lifecycle.ViewModel 4 | import com.jocmp.capy.Account 5 | 6 | class SettingsViewModel( 7 | private val account: Account, 8 | ) : ViewModel() { 9 | val feeds = account.allFeeds 10 | 11 | fun toggleNotifications(feedID: String, enabled: Boolean) { 12 | account.toggleNotifications(feedID = feedID, enabled = enabled) 13 | } 14 | 15 | fun selectAllFeedNotifications() { 16 | account.toggleAllFeedNotifications(enabled = true) 17 | } 18 | 19 | fun deselectAllFeedNotifications() { 20 | account.toggleAllFeedNotifications(enabled = false) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/ui/widget/HeadlinesWidgetReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.ui.widget 2 | 3 | import androidx.glance.appwidget.GlanceAppWidget 4 | import androidx.glance.appwidget.GlanceAppWidgetReceiver 5 | 6 | class HeadlinesWidgetReceiver : GlanceAppWidgetReceiver() { 7 | override val glanceAppWidget: GlanceAppWidget = HeadlinesWidget() 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/capyreader/app/ui/widget/WidgetUpdater.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.ui.widget 2 | 3 | import android.content.Context 4 | import androidx.glance.appwidget.updateAll 5 | 6 | object WidgetUpdater { 7 | suspend fun update(context: Context) { 8 | HeadlinesWidget().updateAll(context) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/me/saket/swipe/defaults.kt: -------------------------------------------------------------------------------- 1 | package me.saket.swipe 2 | 3 | internal const val animationDurationMs = 4_00 4 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/favicon_badge_placeholder.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_rounded_close.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_article_filled.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_open_in_new.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_rounded_arrow_downward.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_rounded_arrow_upward.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/newsmode.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/rss_logo.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #E2D4B0 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/xml-v31/headlines_widget.xml: -------------------------------------------------------------------------------- 1 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/xml/file_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 10 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/xml/headlines_widget.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/test/java/com/capyreader/app/refresher/RefreshIntervalTest.kt: -------------------------------------------------------------------------------- 1 | package com.capyreader.app.refresher 2 | 3 | import com.capyreader.app.refresher.RefreshInterval.EVERY_12_HOURS 4 | import com.capyreader.app.refresher.RefreshInterval.MANUALLY_ONLY 5 | import com.capyreader.app.refresher.RefreshInterval.ON_START 6 | import org.junit.Assert.assertFalse 7 | import org.junit.Assert.assertTrue 8 | import org.junit.Test 9 | 10 | class RefreshIntervalTest { 11 | @Test 12 | fun isPeriodic_manual() { 13 | assertFalse(MANUALLY_ONLY.isPeriodic) 14 | } 15 | 16 | @Test 17 | fun isPeriodic_onStart() { 18 | assertFalse(ON_START.isPeriodic) 19 | } 20 | 21 | @Test 22 | fun isPeriodic_periodic() { 23 | assertTrue(EVERY_12_HOURS.isPeriodic) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/test/java/com/jocmp/capyreader/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jocmp/capyreader/699c32b80716b72a4fdf1dd650cf9f3e7e10f001/app/src/test/java/com/jocmp/capyreader/.gitkeep -------------------------------------------------------------------------------- /article_forge/.tool-versions: -------------------------------------------------------------------------------- 1 | ruby 3.2.3 2 | -------------------------------------------------------------------------------- /article_forge/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "rackup", "~> 2.2.1", require: false 4 | gem "puma", "~> 6.5.0", require: false 5 | gem "sinatra", "~> 4.1.1", require: false 6 | gem "liquid" 7 | gem "rexml", "~> 3.3.9" 8 | 9 | group :development, :test do 10 | gem "sass-embedded" 11 | gem "rerun" 12 | gem "terminal-notifier" 13 | gem "rb-fsevent", "~> 0.11.2" 14 | gem "standard" 15 | gem "debug" 16 | end 17 | -------------------------------------------------------------------------------- /article_forge/README.md: -------------------------------------------------------------------------------- 1 | # Article Forge 2 | 3 | A testbed to modify the HTML layout and stylesheet for [Capy Reader](https://github.com/jocmp/capyreader). 4 | 5 | ## Getting started 6 | 7 | 1. Ensure you have Ruby installed with the version found in `.ruby-version`. 8 | 9 | 2. Run the following command to start the server and navigate to http://127.0.0.1:4567. 10 | 11 | ```sh 12 | make forge 13 | ``` 14 | 15 | ## Fonts 16 | 17 | The following fonts are licensed under the [SIL Open Font License](https://openfontlicense.org/) 18 | 19 | - [Vollkorn](fonts.google.com/specimen/Vollkorn) 20 | - [Atkinson Hyperlegible](https://fonts.google.com/specimen/Atkinson+Hyperlegible) 21 | - [Poppins](https://fonts.google.com/specimen/Poppins) 22 | -------------------------------------------------------------------------------- /article_forge/font_picker.rb: -------------------------------------------------------------------------------- 1 | class FontPicker 2 | def self.pick(family) 3 | family&.downcase || "default" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /article_forge/public/assets/debug.js: -------------------------------------------------------------------------------- 1 | function android() { 2 | /** 3 | * @param {string} src 4 | */ 5 | function openImage(src, caption = null) { 6 | window.open(`/image?src=${btoa(src)}&caption=${caption || ""}`); 7 | } 8 | 9 | function showLinkDialog(href, text) { 10 | console.log('link=', href, "text=", text); 11 | } 12 | 13 | return { 14 | showLinkDialog, 15 | openImage, 16 | }; 17 | } 18 | 19 | function mercuryStub() { 20 | return { 21 | addExtractor: (extractor) => console.log(`[DEBUG] Add extractor for ${extractor.domain}...`) 22 | } 23 | } 24 | 25 | window.Mercury = mercuryStub(); 26 | window.Android = android(); 27 | -------------------------------------------------------------------------------- /article_forge/public/assets/play-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /article_forge/public/res/font/atkinson_hyperlegible.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jocmp/capyreader/699c32b80716b72a4fdf1dd650cf9f3e7e10f001/article_forge/public/res/font/atkinson_hyperlegible.ttf -------------------------------------------------------------------------------- /article_forge/public/res/font/inter.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jocmp/capyreader/699c32b80716b72a4fdf1dd650cf9f3e7e10f001/article_forge/public/res/font/inter.ttf -------------------------------------------------------------------------------- /article_forge/public/res/font/jost.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jocmp/capyreader/699c32b80716b72a4fdf1dd650cf9f3e7e10f001/article_forge/public/res/font/jost.ttf -------------------------------------------------------------------------------- /article_forge/public/res/font/literata.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jocmp/capyreader/699c32b80716b72a4fdf1dd650cf9f3e7e10f001/article_forge/public/res/font/literata.ttf -------------------------------------------------------------------------------- /article_forge/public/res/font/poppins.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jocmp/capyreader/699c32b80716b72a4fdf1dd650cf9f3e7e10f001/article_forge/public/res/font/poppins.ttf -------------------------------------------------------------------------------- /article_forge/public/res/font/vollkorn.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jocmp/capyreader/699c32b80716b72a4fdf1dd650cf9f3e7e10f001/article_forge/public/res/font/vollkorn.ttf -------------------------------------------------------------------------------- /article_forge/script/generate-android-style: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "fileutils" 4 | require "./style_minifier" 5 | 6 | def main 7 | FileUtils.mkdir_p("dist") 8 | StyleMinifier.minify(destination: "dist") 9 | end 10 | 11 | main 12 | -------------------------------------------------------------------------------- /article_forge/style_minifier.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "sass-embedded" 4 | 5 | class StyleMinifier 6 | BASE_NAME = "stylesheet" 7 | 8 | def self.minify(destination: "public/assets") 9 | compressed_css = Sass.compile("style/#{BASE_NAME}.scss", style: :compressed).css 10 | compressed_css = compressed_css.gsub('url("/', 'url("https://appassets.androidplatform.net/') 11 | 12 | File.write("#{destination}/#{File.basename("#{BASE_NAME}.css")}", compressed_css) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /article_forge/views/image.liquid: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 |
{{image_caption}}
11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | plugins { 3 | id("com.android.application") version "8.6.1" apply false 4 | id("com.android.library") version "8.6.1" apply false 5 | id("org.jetbrains.kotlin.android") version libs.versions.kotlin apply false 6 | id("org.jetbrains.kotlin.jvm") version libs.versions.kotlin apply false 7 | id("org.jetbrains.kotlin.plugin.serialization") version libs.versions.kotlin 8 | id("com.google.gms.google-services") version "4.4.2" apply false 9 | id("com.google.firebase.crashlytics") version "3.0.2" apply false 10 | alias(libs.plugins.compose.compiler) apply false 11 | } 12 | -------------------------------------------------------------------------------- /bumpver.toml: -------------------------------------------------------------------------------- 1 | [bumpver] 2 | current_version = "2025.06.1139" 3 | version_pattern = "YYYY.0M.BUILD[-TAG]" 4 | commit_message = "Bump version {old_version} to {new_version}" 5 | commit = true 6 | tag = true 7 | push = false 8 | 9 | [bumpver.file_patterns] 10 | "bumpver.toml" = [ 11 | 'current_version = "{version}"', 12 | ] 13 | "app/build.gradle.kts" = [ 14 | 'versionName = "{version}"', 15 | 'versionCode = BUILD' 16 | ] 17 | -------------------------------------------------------------------------------- /capy/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /capy/proguard-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jocmp/capyreader/699c32b80716b72a4fdf1dd650cf9f3e7e10f001/capy/proguard-rules.pro -------------------------------------------------------------------------------- /capy/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /capy/src/main/assets/play-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /capy/src/main/java/com/jocmp/capy/AccountCountExt.kt: -------------------------------------------------------------------------------- 1 | package com.jocmp.capy 2 | 3 | fun Account.countAll(status: ArticleStatus) = articleRecords.countAll(status) 4 | -------------------------------------------------------------------------------- /capy/src/main/java/com/jocmp/capy/AccountFindArticleIndexExt.kt: -------------------------------------------------------------------------------- 1 | package com.jocmp.capy 2 | 3 | import com.jocmp.capy.articles.UnreadSortOrder 4 | import kotlinx.coroutines.flow.Flow 5 | import java.time.OffsetDateTime 6 | 7 | fun Account.findArticlePages( 8 | articleID: String, 9 | filter: ArticleFilter, 10 | query: String? = null, 11 | unreadSort: UnreadSortOrder = UnreadSortOrder.NEWEST_FIRST, 12 | since: OffsetDateTime = OffsetDateTime.now() 13 | ): Flow { 14 | return articleRecords 15 | .findPages( 16 | articleID = articleID, 17 | filter = filter, 18 | query = query, 19 | unreadSort = unreadSort, 20 | since = since 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /capy/src/main/java/com/jocmp/capy/AccountLatestArticlesExt.kt: -------------------------------------------------------------------------------- 1 | package com.jocmp.capy 2 | 3 | import app.cash.sqldelight.coroutines.asFlow 4 | import app.cash.sqldelight.coroutines.mapToList 5 | import com.jocmp.capy.articles.UnreadSortOrder 6 | import kotlinx.coroutines.Dispatchers 7 | 8 | val Account.latestArticles 9 | get() = 10 | articleRecords 11 | .byStatus 12 | .all( 13 | status = ArticleStatus.UNREAD, 14 | query = null, 15 | since = null, 16 | limit = 10, 17 | unreadSort = UnreadSortOrder.NEWEST_FIRST, 18 | offset = 0, 19 | ) 20 | .asFlow() 21 | .mapToList(Dispatchers.IO) 22 | -------------------------------------------------------------------------------- /capy/src/main/java/com/jocmp/capy/ArticleNotification.kt: -------------------------------------------------------------------------------- 1 | package com.jocmp.capy 2 | 3 | data class ArticleNotification( 4 | val id: Int, 5 | val articleID: String, 6 | val title: String, 7 | val feedID: String, 8 | val feedTitle: String, 9 | val feedFaviconURL: String?, 10 | ) 11 | -------------------------------------------------------------------------------- /capy/src/main/java/com/jocmp/capy/ArticlePages.kt: -------------------------------------------------------------------------------- 1 | package com.jocmp.capy 2 | 3 | data class ArticlePages( 4 | val previousID: String? = null, 5 | val current: Int, 6 | val nextID: String? = null, 7 | val size: Int, 8 | ) { 9 | val previous = current - 1 10 | val next = current + 1 11 | } 12 | -------------------------------------------------------------------------------- /capy/src/main/java/com/jocmp/capy/ArticleStatus.kt: -------------------------------------------------------------------------------- 1 | package com.jocmp.capy 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | enum class ArticleStatus { 7 | ALL, 8 | UNREAD, 9 | STARRED 10 | } 11 | -------------------------------------------------------------------------------- /capy/src/main/java/com/jocmp/capy/Countable.kt: -------------------------------------------------------------------------------- 1 | package com.jocmp.capy 2 | 3 | interface Countable { 4 | val count: Long 5 | } 6 | -------------------------------------------------------------------------------- /capy/src/main/java/com/jocmp/capy/DatabaseProvider.kt: -------------------------------------------------------------------------------- 1 | package com.jocmp.capy 2 | 3 | import com.jocmp.capy.db.Database 4 | 5 | interface DatabaseProvider { 6 | fun build(accountID: String): Database 7 | 8 | fun delete(accountID: String) 9 | } 10 | -------------------------------------------------------------------------------- /capy/src/main/java/com/jocmp/capy/EditFeedFormEntry.kt: -------------------------------------------------------------------------------- 1 | package com.jocmp.capy 2 | 3 | data class EditFeedFormEntry( 4 | val feedID: String, 5 | val title: String, 6 | val folderTitles: List = emptyList() 7 | ) 8 | -------------------------------------------------------------------------------- /capy/src/main/java/com/jocmp/capy/EditFolderFormEntry.kt: -------------------------------------------------------------------------------- 1 | package com.jocmp.capy 2 | 3 | data class EditFolderFormEntry( 4 | val previousTitle: String, 5 | val folderTitle: String, 6 | ) 7 | -------------------------------------------------------------------------------- /capy/src/main/java/com/jocmp/capy/Enclosure.kt: -------------------------------------------------------------------------------- 1 | package com.jocmp.capy 2 | 3 | import java.net.URL 4 | 5 | data class Enclosure( 6 | val url: URL, 7 | val type: String, 8 | val itunesDurationSeconds: Long?, 9 | val itunesImage: String? 10 | ) 11 | -------------------------------------------------------------------------------- /capy/src/main/java/com/jocmp/capy/Feed.kt: -------------------------------------------------------------------------------- 1 | package com.jocmp.capy 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class Feed( 7 | val id: String, 8 | val subscriptionID: String, 9 | val title: String, 10 | val feedURL: String, 11 | val siteURL: String = "", 12 | val folderName: String = "", 13 | val faviconURL: String? = null, 14 | /** Unread count */ 15 | override val count: Long = 0, 16 | val enableStickyFullContent: Boolean = false, 17 | val enableNotifications: Boolean = false, 18 | val folderExpanded: Boolean = false, 19 | ): Countable 20 | -------------------------------------------------------------------------------- /capy/src/main/java/com/jocmp/capy/Folder.kt: -------------------------------------------------------------------------------- 1 | package com.jocmp.capy 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class Folder( 7 | val title: String, 8 | val feeds: List = emptyList(), 9 | val expanded: Boolean = false, 10 | override val count: Long = 0, 11 | ) : Countable 12 | -------------------------------------------------------------------------------- /capy/src/main/java/com/jocmp/capy/OPMLFile.kt: -------------------------------------------------------------------------------- 1 | package com.jocmp.capy 2 | 3 | class OPMLFile( 4 | val account: Account 5 | ) { 6 | suspend fun opmlDocument(): String { 7 | val openingText = """ 8 | | 9 | | 10 | | 11 | | 12 | | Capy Reader Export 13 | | 14 | | 15 | | 16 | """.trimMargin() 17 | 18 | val closingText = """ 19 | | 20 | | 21 | """.trimMargin() 22 | 23 | return openingText + account.asOPML() + closingText 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /capy/src/main/java/com/jocmp/capy/PreferenceStoreProvider.kt: -------------------------------------------------------------------------------- 1 | package com.jocmp.capy 2 | 3 | interface PreferenceStoreProvider { 4 | fun build(accountID: String): AccountPreferences 5 | 6 | fun delete(accountID: String) 7 | } 8 | -------------------------------------------------------------------------------- /capy/src/main/java/com/jocmp/capy/RandomUUID.kt: -------------------------------------------------------------------------------- 1 | package com.jocmp.capy 2 | 3 | import java.util.UUID as JavaUUID 4 | 5 | object RandomUUID { 6 | fun generate(): String { 7 | return JavaUUID.randomUUID().toString() 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /capy/src/main/java/com/jocmp/capy/SavedSearch.kt: -------------------------------------------------------------------------------- 1 | package com.jocmp.capy 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class SavedSearch( 7 | val id: String, 8 | val name: String, 9 | val query: String?, 10 | ) 11 | -------------------------------------------------------------------------------- /capy/src/main/java/com/jocmp/capy/UserAgentInterceptor.kt: -------------------------------------------------------------------------------- 1 | package com.jocmp.capy 2 | 3 | import okhttp3.Interceptor 4 | import okhttp3.Interceptor.Chain 5 | import okhttp3.Response 6 | 7 | class UserAgentInterceptor : Interceptor { 8 | override fun intercept(chain: Chain): Response { 9 | val originalRequest = chain.request() 10 | val requestWithUserAgent = originalRequest.newBuilder() 11 | .header("User-Agent", USER_AGENT) 12 | .build() 13 | return chain.proceed(requestWithUserAgent) 14 | } 15 | 16 | companion object { 17 | const val USER_AGENT = "CapyReader (RSS Reader; https://capyreader.com/)" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /capy/src/main/java/com/jocmp/capy/accounts/AutoDelete.kt: -------------------------------------------------------------------------------- 1 | package com.jocmp.capy.accounts 2 | 3 | enum class AutoDelete { 4 | DISABLED, 5 | WEEKLY, 6 | EVERY_TWO_WEEKS, 7 | EVERY_MONTH, 8 | EVERY_THREE_MONTHS; 9 | 10 | companion object { 11 | val default = EVERY_THREE_MONTHS 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /capy/src/main/java/com/jocmp/capy/accounts/BasicAuthInterceptor.kt: -------------------------------------------------------------------------------- 1 | package com.jocmp.capy.accounts 2 | 3 | import okhttp3.Interceptor 4 | 5 | class BasicAuthInterceptor(private val credentials: () -> String) : Interceptor { 6 | override fun intercept(chain: Interceptor.Chain): okhttp3.Response { 7 | val request = chain.request() 8 | 9 | if (request.headers("Authorization").isEmpty()) { 10 | val authenticatedRequest = 11 | request.newBuilder().header("Authorization", credentials()).build() 12 | return chain.proceed(authenticatedRequest) 13 | } 14 | 15 | return chain.proceed(request) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /capy/src/main/java/com/jocmp/capy/accounts/FaviconFetcher.kt: -------------------------------------------------------------------------------- 1 | package com.jocmp.capy.accounts 2 | 3 | interface FaviconFetcher { 4 | suspend fun isValid(url: String?): Boolean 5 | } 6 | -------------------------------------------------------------------------------- /capy/src/main/java/com/jocmp/capy/accounts/FeedOPMLExt.kt: -------------------------------------------------------------------------------- 1 | package com.jocmp.capy.accounts 2 | 3 | import com.jocmp.capy.Feed 4 | import com.jocmp.capy.common.escapingSpecialXMLCharacters 5 | import com.jocmp.capy.common.prepending 6 | 7 | internal fun Feed.asOPML(indentLevel: Int): String { 8 | val parsedSiteURL = siteURL.escapingSpecialXMLCharacters 9 | val parsedFeedURL = feedURL.escapingSpecialXMLCharacters 10 | val parsedName = title.escapingSpecialXMLCharacters 11 | 12 | val opml = 13 | "\n" 14 | return opml.prepending(tabCount = indentLevel) 15 | } 16 | -------------------------------------------------------------------------------- /capy/src/main/java/com/jocmp/capy/accounts/FreshRSSPath.kt: -------------------------------------------------------------------------------- 1 | package com.jocmp.capy.accounts 2 | 3 | fun withFreshRSSPath(url: String, source: Source): String { 4 | if (source != Source.FRESHRSS || url.endsWith(FRESHRSS_PATH)) { 5 | return url 6 | } 7 | 8 | return "${url}${FRESHRSS_PATH}" 9 | } 10 | 11 | const val FRESHRSS_PATH = "api/greader.php/" 12 | -------------------------------------------------------------------------------- /capy/src/main/java/com/jocmp/capy/accounts/HttpClientBuilder.kt: -------------------------------------------------------------------------------- 1 | package com.jocmp.capy.accounts 2 | 3 | import com.jocmp.capy.UserAgentInterceptor 4 | import okhttp3.Cache 5 | import okhttp3.OkHttpClient 6 | import java.io.File 7 | import java.net.URI 8 | 9 | fun httpClientBuilder(cachePath: URI) = OkHttpClient 10 | .Builder() 11 | .addInterceptor(UserAgentInterceptor()) 12 | .cache( 13 | Cache( 14 | directory = File(File(cachePath), "http_cache"), 15 | maxSize = 50L * 1024L * 1024L // 50 MiB 16 | ) 17 | ) 18 | -------------------------------------------------------------------------------- /capy/src/main/java/com/jocmp/capy/accounts/Source.kt: -------------------------------------------------------------------------------- 1 | package com.jocmp.capy.accounts 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | enum class Source(val value: String) { 7 | LOCAL("local"), 8 | FEEDBIN("feedbin"), 9 | FRESHRSS("freshrss"), 10 | READER("reader"); 11 | 12 | val hasCustomURL 13 | get() = this == FRESHRSS || this == READER 14 | } 15 | -------------------------------------------------------------------------------- /capy/src/main/java/com/jocmp/capy/accounts/SubscriptionChoice.kt: -------------------------------------------------------------------------------- 1 | package com.jocmp.capy.accounts 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | internal data class SubscriptionChoice( 7 | val feed_url: String, 8 | val title: String, 9 | ) 10 | -------------------------------------------------------------------------------- /capy/src/main/java/com/jocmp/capy/accounts/local/RichMedia.kt: -------------------------------------------------------------------------------- 1 | package com.jocmp.capy.accounts.local 2 | 3 | import com.jocmp.rssparser.model.RssItem 4 | 5 | internal object RichMedia { 6 | fun parse(item: RssItem): String? { 7 | val media = item.media ?: return null 8 | val videoID = item.youtubeVideoID ?: return null 9 | val description = media.description ?: return null 10 | 11 | return """ 12 |
13 |