├── .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 |
5 |
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 |
5 |
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 |
13 |
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 |
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 |
14 |
$description
15 |
16 | """.trimIndent()
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/accounts/reader/InvalidFoldersError.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.accounts.reader
2 |
3 | class InvalidFoldersError: IllegalArgumentException()
4 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/accounts/reader/ReaderOkHttpClient.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.accounts.reader
2 |
3 | import com.jocmp.capy.AccountPreferences
4 | import com.jocmp.capy.accounts.BasicAuthInterceptor
5 | import com.jocmp.capy.accounts.httpClientBuilder
6 | import okhttp3.OkHttpClient
7 | import java.net.URI
8 |
9 | internal object ReaderOkHttpClient {
10 | fun forAccount(path: URI, preferences: AccountPreferences): OkHttpClient {
11 | return httpClientBuilder(cachePath = path)
12 | .addInterceptor(
13 | BasicAuthInterceptor {
14 | val secret = preferences.password.get()
15 |
16 | "GoogleLogin auth=${secret}"
17 | }
18 | )
19 | .build()
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/accounts/reader/TagNameExt.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.accounts.reader
2 |
3 | import com.jocmp.readerclient.Tag
4 |
5 | val Tag.name: String
6 | get() = id.split("/").lastOrNull().orEmpty()
7 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/articles/ArticleImageEnclosuresExt.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.articles
2 |
3 | import com.jocmp.capy.Article
4 | import org.jsoup.nodes.Element
5 |
6 | fun Article.imageEnclosures(): Element? {
7 | val images = enclosures.filter { it.type.startsWith("image/") }
8 |
9 | if (images.isEmpty()) {
10 | return null
11 | }
12 |
13 | return Element("div").apply {
14 | enclosures.forEach { enclosure ->
15 | val image = Element("img").apply {
16 | attr("src", enclosure.url.toString())
17 | }
18 |
19 | appendChild(image)
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/articles/CleanStyles.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.articles
2 |
3 | import org.jsoup.nodes.Document
4 |
5 | fun cleanStyles(document: Document) {
6 | document.select("#article-body-content *").forEach {
7 | it.removeAttr("style")
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/articles/FontOption.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.articles
2 |
3 | enum class FontOption {
4 | SYSTEM_DEFAULT,
5 | ATKINSON_HYPERLEGIBLE,
6 | INTER,
7 | JOST,
8 | LITERATA,
9 | POPPINS,
10 | VOLLKORN;
11 |
12 | val slug: String
13 | get() = when(this) {
14 | SYSTEM_DEFAULT -> "default"
15 | ATKINSON_HYPERLEGIBLE -> "atkinson_hyperlegible"
16 | INTER -> "inter"
17 | JOST -> "jost"
18 | LITERATA -> "literata"
19 | POPPINS -> "poppins"
20 | VOLLKORN -> "vollkorn"
21 | }
22 |
23 | companion object {
24 | val default = SYSTEM_DEFAULT
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/articles/FontSize.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.articles
2 |
3 | object FontSize {
4 | private const val MIN = 12
5 | private const val MAX = 32
6 |
7 | val scale: List
8 | get() = (MIN..MAX step 2).toList()
9 |
10 | const val DEFAULT = 16
11 | }
12 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/articles/HtmlPostProcessor.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.articles
2 |
3 | import org.jsoup.nodes.Document
4 |
5 | object HtmlPostProcessor {
6 | fun clean(document: Document, hideImages: Boolean) {
7 | cleanStyles(document)
8 | if (hideImages) {
9 | removeImages(document)
10 | }
11 | cleanLinks(document)
12 | wrapTables(document)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/articles/ParseHTML.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.articles
2 |
3 | import com.jocmp.capy.Article
4 | import org.json.JSONObject
5 | import org.jsoup.nodes.Document
6 |
7 | fun parseHtml(article: Article, document: Document, hideImages: Boolean): String {
8 | val html = document.html()
9 |
10 | return """
11 |
18 | """.trimIndent()
19 | }
20 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/articles/RemoveImages.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.articles
2 |
3 | import org.jsoup.nodes.Document
4 |
5 | fun removeImages(document: Document) {
6 | document.select("img").forEach {
7 | it.remove()
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/articles/Sort.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.articles
2 |
3 | enum class Sort {
4 | OldestFirst,
5 | NewestFirst,
6 | }
7 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/articles/UnreadSortOrder.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.articles
2 |
3 | enum class UnreadSortOrder {
4 | NEWEST_FIRST,
5 | OLDEST_FIRST;
6 |
7 | companion object {
8 | val default = NEWEST_FIRST
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/articles/WrapTables.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.articles
2 |
3 | import org.jsoup.nodes.Document
4 |
5 | internal fun wrapTables(document: Document) {
6 | document.select("table").forEach { table ->
7 | val wrapper = document.createElement("div")
8 |
9 | wrapper.addClass("table__wrapper")
10 |
11 | table.wrap(wrapper.outerHtml())
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/common/Async.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.common
2 |
3 | sealed class Async(private val value: T?) {
4 | open operator fun invoke(): T? = value
5 |
6 | data object Uninitialized : Async(value = null)
7 |
8 | data object Loading : Async(value = null)
9 |
10 | data class Success(private val value: T) : Async(value = value) {
11 | override operator fun invoke(): T = value
12 | }
13 |
14 | data class Failure(val error: Throwable, private val value: T? = null) : Async(value = value)
15 | }
16 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/common/DataseTransactionExt.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.common
2 |
3 | import app.cash.sqldelight.TransactionWithoutReturn
4 | import com.jocmp.capy.db.Database
5 | import com.jocmp.capy.logging.CapyLog
6 |
7 | fun Database.transactionWithErrorHandling(
8 | body: TransactionWithoutReturn.() -> Unit
9 | ) {
10 | try {
11 | transaction(noEnclosing = false, body)
12 | } catch(e: Exception) {
13 | CapyLog.error("db_error", e)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/common/DecodeHTML.kt:
--------------------------------------------------------------------------------
1 |
2 |
3 | package com.jocmp.capy.common
4 |
5 | import android.text.Html
6 |
7 | @Suppress("DEPRECATION")
8 | fun decodeHTML(text: String): String {
9 | return Html.fromHtml(text).toString()
10 | }
11 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/common/FeedListExt.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.common
2 |
3 | import com.jocmp.capy.Feed
4 | import java.text.Collator
5 | import java.util.Locale
6 |
7 | fun List.sortedByTitle() =
8 | sortedWith(compareBy(Collator.getInstance(Locale.getDefault()).apply {
9 | strength = Collator.PRIMARY
10 | }) {
11 | it.title
12 | })
13 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/common/FolderListExt.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.common
2 |
3 | import com.jocmp.capy.Folder
4 |
5 | fun List.sortedByTitle() =
6 | sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.title })
7 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/common/LocalDateTimeExt.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.common
2 |
3 | import java.time.ZonedDateTime
4 | import java.util.TimeZone
5 |
6 | fun ZonedDateTime.toDeviceDateTime(): ZonedDateTime {
7 | return withZoneSameInstant(TimeZone.getDefault().toZoneId())
8 | }
9 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/common/MutableListExt.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.common
2 |
3 | fun MutableList.replace(element: T) {
4 | val index = indexOf(element)
5 |
6 | if (index > -1) {
7 | set(index, element)
8 | }
9 | }
10 |
11 | fun MutableList.upsert(element: T) {
12 | val index = indexOf(element)
13 |
14 | if (index > -1) {
15 | set(index, element)
16 | } else {
17 | add(element)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/common/MutableSetExt.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.common
2 |
3 | fun MutableSet.replace(element: T) {
4 | if (remove(element)) {
5 | add(element)
6 | }
7 | }
8 |
9 | fun MutableSet.upsert(element: T) {
10 | remove(element)
11 | add(element)
12 | }
13 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/common/NetworkErrorExt.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.common
2 |
3 | import java.io.IOException
4 |
5 |
6 | val Throwable.isIOError
7 | get() = this is IOException
8 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/common/OptionalFile.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.common
2 |
3 | import java.io.File
4 |
5 |
6 | fun optionalFile(string: String?): File? {
7 | if (string.isNullOrBlank()) {
8 | return null
9 | }
10 |
11 | return try {
12 | File(string)
13 | } catch (_: Throwable) {
14 | null
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/common/OptionalURL.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.common
2 |
3 | import java.net.URL
4 |
5 | fun optionalURL(string: String?): URL? {
6 | if (string.isNullOrBlank()) {
7 | return null
8 | }
9 |
10 | return try {
11 | URL(string)
12 | } catch (_: Throwable) {
13 | null
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/common/SavedSearchListExt.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.common
2 |
3 | import com.jocmp.capy.SavedSearch
4 |
5 | internal fun List.sortedByName() =
6 | sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
7 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/common/StringPrependingExt.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.common
2 |
3 | internal fun String.prepending(tabCount: Int): String {
4 | if (tabCount < 1) {
5 | return this
6 | }
7 |
8 | return "${repeatTab(tabCount)}$this"
9 | }
10 |
11 | internal fun repeatTab(tabCount: Int): String {
12 | return " ".repeat(2 * tabCount)
13 | }
14 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/common/StringProtocolExt.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.common
2 |
3 | val String.withTrailingSeparator: String
4 | get() {
5 | return if (endsWith("/")) {
6 | this
7 | } else {
8 | "$this/"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/common/SubscriptionHostExt.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.common
2 |
3 | import com.jocmp.feedbinclient.Subscription
4 | import java.net.MalformedURLException
5 | import java.net.URL
6 |
7 | internal val Subscription.host: String?
8 | get() {
9 | return try {
10 | URL(site_url).host
11 | } catch (e: MalformedURLException) {
12 | null
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/common/UnauthorizedError.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.common
2 |
3 | /**
4 | * Throw when a source returns a 401/Unauthorized response
5 | */
6 | class UnauthorizedError: Error()
7 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/common/WindowOrigin.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.common
2 |
3 | import java.net.MalformedURLException
4 | import java.net.URL
5 |
6 | /**
7 | * https://developer.mozilla.org/en-US/docs/Web/API/Window/origin
8 | *
9 | * Returns the URL's scheme/host/port
10 | */
11 | fun windowOrigin(url: URL?): String? {
12 | if (url == null || !(url.protocol == "http" || url.protocol == "https")) {
13 | return null
14 | }
15 |
16 | return try {
17 | URL(url.protocol, url.host, url.port, "", null).toString()
18 | } catch (e: MalformedURLException) {
19 | null
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/common/WithResult.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.common
2 |
3 | import retrofit2.Response
4 |
5 | internal fun withResult(response: Response, handler: (result: T) -> Unit) {
6 | val result = response.body()
7 |
8 | if (response.code() == 401) {
9 | throw UnauthorizedError()
10 | }
11 |
12 | if (!response.isSuccessful || result == null) {
13 | return
14 | }
15 |
16 | handler(result)
17 | }
18 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/logging/CapyLog.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.logging
2 |
3 | import android.util.Log
4 |
5 | object CapyLog : Logging {
6 | override fun info(tag: String, data: Map) {
7 | Log.i(appTag(tag), serializeData(data))
8 | }
9 |
10 | override fun warn(tag: String, data: Map) {
11 | Log.w(appTag(tag), serializeData(data))
12 | }
13 |
14 | override fun error(tag: String, error: Throwable, data: Map) {
15 | Log.e(appTag(tag), serializeData(data), error)
16 | }
17 |
18 | private fun serializeData(data: Map): String {
19 | return data.map { (key, value) -> "$key=$value" }.joinToString(" ")
20 | }
21 |
22 | private fun appTag(path: String) = "cr.$path"
23 | }
24 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/logging/Logging.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.logging
2 |
3 | interface Logging {
4 | fun info(tag: String, data: Map = emptyMap())
5 |
6 | fun warn(tag: String, data: Map = emptyMap())
7 |
8 | fun error(tag: String, error: Throwable, data: Map = emptyMap())
9 | }
10 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/opml/Feed.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.opml
2 |
3 | internal data class Feed(
4 | val id: String? = null,
5 | val title: String? = null,
6 | val text: String? = null,
7 | val htmlUrl: String? = null,
8 | val xmlUrl: String? = null,
9 | )
10 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/opml/FeedOutlineExt.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.opml
2 |
3 | import com.jocmp.capy.Feed
4 | import com.jocmp.capy.db.Feeds as DBFeed
5 | import com.jocmp.capy.opml.Feed as OPMLFeed
6 |
7 | internal fun OPMLFeed.asFeed(feeds: Map = mapOf()): Feed? {
8 | val parsedID = id?.toLongOrNull() ?: return null
9 | val feed = feeds[parsedID]
10 |
11 | feed ?: return null
12 |
13 | return Feed(
14 | id = feed.id,
15 | subscriptionID = feed.subscription_id,
16 | title = title ?: "",
17 | feedURL = xmlUrl ?: ""
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/opml/Folder.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.opml
2 |
3 | internal data class Folder(
4 | val title: String? = null,
5 | val text: String? = null,
6 | val feeds: MutableList = mutableListOf(),
7 | val folders: MutableList = mutableListOf()
8 | )
9 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/opml/FolderOutlineExt.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.opml
2 |
3 | import com.jocmp.capy.db.Feeds as DBFeed
4 | import com.jocmp.capy.Folder
5 |
6 | internal fun Outline.FolderOutline.asFolder(feeds: Map): Folder {
7 | return Folder(
8 | title = folder.title ?: "",
9 | feeds = folder.feeds.mapNotNull {
10 | it.asFeed(feeds = feeds)
11 | }.toMutableList()
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/opml/OPMLDocument.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.opml
2 |
3 | internal data class OPMLDocument(
4 | val outlines: MutableList = mutableListOf()
5 | )
6 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/opml/Outline.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.opml
2 |
3 | internal sealed class Outline {
4 | abstract val title: String
5 |
6 | data class FolderOutline(val folder: Folder) : Outline() {
7 | override val title: String
8 | get() = folder.title ?: ""
9 | }
10 |
11 | data class FeedOutline(val feed: Feed) : Outline() {
12 | override val title: String
13 | get() = feed.title ?: ""
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/persistence/ArticleNotificationMapper.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.persistence
2 |
3 | import com.jocmp.capy.ArticleNotification
4 |
5 | internal fun articleNotificationMapper(
6 | articleID: String,
7 | title: String?,
8 | summary: String?,
9 | feedID: String?,
10 | feedTitle: String?,
11 | feedFavicon: String?,
12 | ) = ArticleNotification(
13 | id = articleID.hashCode(),
14 | articleID = articleID,
15 | title = title.orEmpty().ifBlank { summary.orEmpty() },
16 | feedID = feedID!!,
17 | feedTitle = feedTitle.orEmpty(),
18 | feedFaviconURL = feedFavicon
19 | )
20 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/persistence/ArticleNotificationRecords.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.persistence
2 |
3 | import com.jocmp.capy.db.Database
4 |
5 | internal class ArticleNotificationRecords internal constructor(
6 | private val database: Database
7 | ) {
8 | fun createNotification() {
9 | }
10 |
11 | fun clearNotification() {
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/persistence/ArticlePageMapper.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.persistence
2 |
3 | import com.jocmp.capy.ArticlePages
4 |
5 | fun articlePageMapper(
6 | previousID: String?,
7 | currentIndex: Long,
8 | nextID: String?,
9 | size: Long,
10 | ) =
11 | ArticlePages(
12 | previousID,
13 | current = currentIndex.toInt(),
14 | nextID,
15 | size.toInt(),
16 | )
17 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/persistence/FolderRecords.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.persistence
2 |
3 | import com.jocmp.capy.db.Database
4 |
5 | internal class FolderRecords(private val database: Database) {
6 | fun expand(folderName: String, expanded: Boolean) {
7 | database.foldersQueries.upsert(
8 | name = folderName,
9 | expanded = expanded,
10 | )
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/persistence/articles/ArticleHelpers.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.persistence.articles
2 |
3 | import com.jocmp.capy.ArticleStatus
4 | import com.jocmp.capy.articles.UnreadSortOrder
5 | import java.time.OffsetDateTime
6 |
7 | internal fun isDescendingOrder(status: ArticleStatus, unreadSort: UnreadSortOrder) =
8 | status != ArticleStatus.UNREAD ||
9 | unreadSort == UnreadSortOrder.NEWEST_FIRST
10 |
11 | internal fun mapLastRead(read: Boolean?, value: OffsetDateTime?): Long? {
12 | if (read != null) {
13 | return value?.toEpochSecond()
14 | }
15 |
16 | return null
17 | }
18 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/persistence/articles/NewestFirst.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.persistence.articles
2 |
3 | import com.jocmp.capy.ArticleStatus
4 | import com.jocmp.capy.articles.UnreadSortOrder
5 |
6 | fun isNewestFirst(status: ArticleStatus, unreadSort: UnreadSortOrder): Boolean {
7 | return status != ArticleStatus.UNREAD ||
8 | unreadSort == UnreadSortOrder.NEWEST_FIRST
9 | }
10 |
--------------------------------------------------------------------------------
/capy/src/main/java/com/jocmp/capy/preferences/Preference.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.preferences
2 |
3 | import kotlinx.coroutines.CoroutineScope
4 | import kotlinx.coroutines.flow.Flow
5 | import kotlinx.coroutines.flow.StateFlow
6 |
7 | interface Preference {
8 |
9 | fun key(): String
10 |
11 | fun get(): T
12 |
13 | fun set(value: T)
14 |
15 | fun isSet(): Boolean
16 |
17 | fun delete()
18 |
19 | fun defaultValue(): T
20 |
21 | fun changes(): Flow
22 |
23 | fun stateIn(scope: CoroutineScope): StateFlow
24 | }
25 |
26 | inline fun Preference.getAndSet(crossinline block: (T) -> R) = set(block(get()))
27 |
--------------------------------------------------------------------------------
/capy/src/main/res/font/atkinson_hyperlegible.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jocmp/capyreader/699c32b80716b72a4fdf1dd650cf9f3e7e10f001/capy/src/main/res/font/atkinson_hyperlegible.ttf
--------------------------------------------------------------------------------
/capy/src/main/res/font/inter.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jocmp/capyreader/699c32b80716b72a4fdf1dd650cf9f3e7e10f001/capy/src/main/res/font/inter.ttf
--------------------------------------------------------------------------------
/capy/src/main/res/font/jost.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jocmp/capyreader/699c32b80716b72a4fdf1dd650cf9f3e7e10f001/capy/src/main/res/font/jost.ttf
--------------------------------------------------------------------------------
/capy/src/main/res/font/literata.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jocmp/capyreader/699c32b80716b72a4fdf1dd650cf9f3e7e10f001/capy/src/main/res/font/literata.ttf
--------------------------------------------------------------------------------
/capy/src/main/res/font/poppins.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jocmp/capyreader/699c32b80716b72a4fdf1dd650cf9f3e7e10f001/capy/src/main/res/font/poppins.ttf
--------------------------------------------------------------------------------
/capy/src/main/res/font/vollkorn.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jocmp/capyreader/699c32b80716b72a4fdf1dd650cf9f3e7e10f001/capy/src/main/res/font/vollkorn.ttf
--------------------------------------------------------------------------------
/capy/src/main/res/xml/network_security_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/capy/src/main/sqldelight/com/jocmp/capy/db/10_AddDismissedAtToArticleNotifications.sqm:
--------------------------------------------------------------------------------
1 | ALTER TABLE article_notifications
2 | ADD COLUMN dismissed_at INTEGER;
3 |
--------------------------------------------------------------------------------
/capy/src/main/sqldelight/com/jocmp/capy/db/11_CreateFolders.sqm:
--------------------------------------------------------------------------------
1 | import kotlin.Boolean;
2 |
3 | -- Folders represent configuration for many taggings
4 | CREATE TABLE folders (
5 | name TEXT NOT NULL PRIMARY KEY,
6 | expanded INTEGER AS Boolean NOT NULL DEFAULT 0
7 | );
8 |
--------------------------------------------------------------------------------
/capy/src/main/sqldelight/com/jocmp/capy/db/12_CreateEnclosures.sqm:
--------------------------------------------------------------------------------
1 | CREATE TABLE enclosures (
2 | url TEXT PRIMARY KEY NOT NULL,
3 | article_id TEXT NOT NULL REFERENCES articles(id),
4 | type TEXT NOT NULL,
5 | itunes_duration_seconds INTEGER,
6 | itunes_image TEXT
7 | );
8 |
--------------------------------------------------------------------------------
/capy/src/main/sqldelight/com/jocmp/capy/db/1_AddFeeds.sqm:
--------------------------------------------------------------------------------
1 | CREATE TABLE feeds (
2 | id TEXT NOT NULL PRIMARY KEY,
3 | subscription_id TEXT NOT NULL UNIQUE,
4 | title TEXT NOT NULL,
5 | feed_url TEXT NOT NULL,
6 | site_url TEXT,
7 | favicon_url TEXT
8 | );
9 |
--------------------------------------------------------------------------------
/capy/src/main/sqldelight/com/jocmp/capy/db/2_AddArticles.sqm:
--------------------------------------------------------------------------------
1 | CREATE TABLE articles(
2 | id TEXT NOT NULL PRIMARY KEY,
3 | feed_id TEXT REFERENCES feeds(id),
4 | title TEXT,
5 | author TEXT,
6 | content_html TEXT,
7 | extracted_content_url TEXT,
8 | url TEXT,
9 | summary TEXT,
10 | image_url TEXT,
11 | published_at INTEGER
12 | );
13 |
14 | CREATE UNIQUE INDEX articles_external_id_feed_id_index ON articles(id, feed_id);
15 |
--------------------------------------------------------------------------------
/capy/src/main/sqldelight/com/jocmp/capy/db/3_AddArticleStatuses.sqm:
--------------------------------------------------------------------------------
1 | import kotlin.Boolean;
2 |
3 | CREATE TABLE article_statuses (
4 | article_id TEXT NOT NULL PRIMARY KEY REFERENCES articles(id),
5 | read INTEGER AS Boolean NOT NULL DEFAULT 0,
6 | starred INTEGER AS Boolean NOT NULL DEFAULT 0,
7 | last_read_at INTEGER,
8 | updated_at INTEGER NOT NULL
9 | );
10 |
--------------------------------------------------------------------------------
/capy/src/main/sqldelight/com/jocmp/capy/db/4_AddTaggings.sqm:
--------------------------------------------------------------------------------
1 | CREATE TABLE taggings (
2 | id TEXT NOT NULL PRIMARY KEY,
3 | feed_id TEXT NOT NULL REFERENCES feeds(id),
4 | name TEXT NOT NULL
5 | );
6 |
--------------------------------------------------------------------------------
/capy/src/main/sqldelight/com/jocmp/capy/db/5_AddFeedStickyContentSetting.sqm:
--------------------------------------------------------------------------------
1 | import kotlin.Boolean;
2 |
3 | ALTER TABLE feeds
4 | ADD COLUMN enable_sticky_full_content INTEGER AS Boolean NOT NULL DEFAULT 0;
5 |
--------------------------------------------------------------------------------
/capy/src/main/sqldelight/com/jocmp/capy/db/6_AddEnableNotificationsToFeeds.sqm:
--------------------------------------------------------------------------------
1 | import kotlin.Boolean;
2 |
3 | ALTER TABLE feeds
4 | ADD COLUMN enable_notifications INTEGER AS Boolean NOT NULL DEFAULT 0;
5 |
--------------------------------------------------------------------------------
/capy/src/main/sqldelight/com/jocmp/capy/db/7_AddArticleNotifications.sqm:
--------------------------------------------------------------------------------
1 | CREATE TABLE article_notifications (
2 | article_id TEXT NOT NULL PRIMARY KEY
3 | );
4 |
--------------------------------------------------------------------------------
/capy/src/main/sqldelight/com/jocmp/capy/db/8_AddSavedSearches.sqm:
--------------------------------------------------------------------------------
1 | CREATE TABLE saved_searches (
2 | id TEXT NOT NULL PRIMARY KEY,
3 | name TEXT NOT NULL,
4 | query_text TEXT
5 | );
6 |
--------------------------------------------------------------------------------
/capy/src/main/sqldelight/com/jocmp/capy/db/9_AddSavedSearchArticles.sqm:
--------------------------------------------------------------------------------
1 | CREATE TABLE saved_search_articles (
2 | saved_search_id TEXT NOT NULL REFERENCES saved_searches(id),
3 | article_id TEXT NOT NULL REFERENCES articles(id),
4 | PRIMARY KEY (saved_search_id, article_id)
5 | );
6 |
--------------------------------------------------------------------------------
/capy/src/main/sqldelight/com/jocmp/capy/db/enclosures.sq:
--------------------------------------------------------------------------------
1 | findByArticleID:
2 | SELECT
3 | enclosures.url,
4 | enclosures.type,
5 | enclosures.itunes_duration_seconds,
6 | enclosures.itunes_image
7 | FROM enclosures
8 | JOIN articles ON articles.id = enclosures.article_id
9 | WHERE articles.id = :articleID;
10 |
11 | create:
12 | INSERT INTO enclosures(
13 | url,
14 | article_id,
15 | type,
16 | itunes_duration_seconds,
17 | itunes_image
18 | )
19 | VALUES (
20 | :url,
21 | :article_id,
22 | :type,
23 | :itunes_duration_seconds,
24 | :itunes_image
25 | )
26 | ON CONFLICT(url) DO NOTHING;
27 |
--------------------------------------------------------------------------------
/capy/src/main/sqldelight/com/jocmp/capy/db/folders.sq:
--------------------------------------------------------------------------------
1 | all:
2 | SELECT *
3 | FROM folders;
4 |
5 | upsert:
6 | INSERT OR REPLACE INTO folders(
7 | name,
8 | expanded
9 | )
10 | VALUES (
11 | :name,
12 | :expanded
13 | );
14 |
15 | find:
16 | SELECT expanded
17 | FROM folders
18 | WHERE name = :name;
19 |
--------------------------------------------------------------------------------
/capy/src/test/java/com/jocmp/capy/ArticleFilterTest.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy
2 |
3 | import org.junit.Test
4 | import kotlin.test.assertEquals
5 | import kotlin.test.assertNotEquals
6 |
7 | class ArticleFilterTest {
8 | @Test
9 | fun withStatus_copiesExistingFilter() {
10 | val articles = ArticleFilter.default()
11 |
12 | val nextFilter = articles.withStatus(status = ArticleStatus.STARRED)
13 |
14 | assertNotEquals(articles.status, nextFilter.status)
15 | assertEquals(expected = ArticleStatus.STARRED, actual = nextFilter.status)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/capy/src/test/java/com/jocmp/capy/IntExt.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy
2 |
3 | import kotlinx.coroutines.runBlocking
4 |
5 | internal fun Int.repeated(action: (i: Int) -> T): List {
6 | val list = mutableListOf()
7 |
8 | repeat(this) {
9 | list.add(action(it))
10 | }
11 |
12 | return list
13 | }
14 |
15 | internal fun Int.awaitRepeated(action: suspend (i: Int) -> T): List {
16 | val list = mutableListOf()
17 |
18 | repeat(this) {
19 | list.add(runBlocking { action(it) })
20 | }
21 |
22 | return list
23 | }
24 |
--------------------------------------------------------------------------------
/capy/src/test/java/com/jocmp/capy/MockFeedFinder.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy
2 |
3 | import com.jocmp.feedfinder.FeedFinder
4 | import com.jocmp.feedfinder.parser.Feed
5 | import com.jocmp.rssparser.model.RssChannel
6 |
7 | class MockFeedFinder(private val sites: Map = emptyMap()) : FeedFinder {
8 | override suspend fun find(url: String): Result> {
9 | val feed = sites[url] ?: return Result.failure(Throwable("No feeds!"))
10 |
11 | return Result.success(listOf(feed))
12 | }
13 |
14 | override suspend fun fetch(url: String): Result {
15 | TODO("Not yet implemented")
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/capy/src/test/java/com/jocmp/capy/PathHelpers.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy
2 |
3 | import java.io.File
4 | import java.net.URI
5 |
6 | fun testResource(resource: String): String {
7 | return "src/test/resources/${resource}"
8 | }
9 |
10 | fun testURI(resource: String): URI {
11 | return File(testResource(resource)).toURI()
12 | }
13 |
14 | fun testFile(resource: String): File {
15 | return File(testResource(resource))
16 | }
17 |
--------------------------------------------------------------------------------
/capy/src/test/java/com/jocmp/capy/RandomID.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy
2 |
3 | import java.security.SecureRandom
4 |
5 | fun randomID() = SecureRandom.getInstanceStrong().nextLong()
6 |
--------------------------------------------------------------------------------
/capy/src/test/java/com/jocmp/capy/accounts/FakeFaviconFetcher.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.accounts
2 |
3 | object FakeFaviconFetcher : FaviconFetcher {
4 | override suspend fun isValid(url: String?) = true
5 | }
6 |
--------------------------------------------------------------------------------
/capy/src/test/java/com/jocmp/capy/articles/HtmlHelpers.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.articles
2 |
3 | import org.jsoup.Jsoup
4 | import org.jsoup.nodes.Document
5 | import kotlin.test.assertEquals
6 |
7 | object HtmlHelpers {
8 | fun html(content: String): Document {
9 | val documentString = """
10 |
11 |
12 | $content
13 |
14 |
15 | """.trimIndent()
16 |
17 | val doc = Jsoup.parse(documentString)
18 | return doc
19 | }
20 |
21 | fun assertEquals(actual: Document, expected: () -> String) {
22 | assertEquals(expected = snapshot(expected()), actual = snapshot(actual.body().html()))
23 | }
24 |
25 | private fun snapshot(content: String): String {
26 | return content.trimIndent()
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/capy/src/test/java/com/jocmp/capy/common/StringPrependingExtTest.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.common
2 |
3 | import org.junit.Test
4 | import kotlin.test.assertEquals
5 |
6 | class StringPrependingExtTest {
7 | @Test
8 | fun prepending() {
9 | val str = "Hello Moto".prepending(tabCount = 2)
10 | val expected = " Hello Moto"
11 |
12 | assertEquals(expected = expected, actual = str)
13 | }
14 |
15 | @Test
16 | fun repeatTab() {
17 | assertEquals(expected = " ", actual = repeatTab(tabCount = 1))
18 | assertEquals(expected = " ", actual = repeatTab(tabCount = 2))
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/capy/src/test/java/com/jocmp/capy/fixtures/FolderFixture.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.fixtures
2 |
3 | import com.jocmp.capy.Feed
4 | import com.jocmp.capy.RandomUUID
5 | import com.jocmp.capy.db.Database
6 | import com.jocmp.capy.persistence.TaggingRecords
7 |
8 | class FolderFixture(private val database: Database) {
9 | private val feedFixture = FeedFixture(database)
10 |
11 | fun create(
12 | name: String = "My Folder",
13 | feed: Feed = feedFixture.create(feedURL = "https://example.com/${RandomUUID.generate()}"),
14 | id: String = "${feed.title}:$name",
15 | ) {
16 | TaggingRecords(database).upsert(
17 | id = id,
18 | feedID = feed.id,
19 | name = name
20 | )
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/capy/src/test/java/com/jocmp/capy/fixtures/GenericFeed.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.capy.fixtures
2 |
3 | import com.jocmp.feedfinder.parser.Feed
4 | import com.jocmp.rssparser.model.RssItem
5 | import java.net.URL
6 |
7 | class GenericFeed(
8 | override val name: String,
9 | url: String,
10 | override val siteURL: URL? = null,
11 | private val valid: Boolean = true,
12 | override val faviconURL: URL? = null,
13 | override val items: List = emptyList(),
14 | ) : Feed {
15 | override fun isValid() = valid
16 |
17 | override val feedURL = URL(url)
18 | }
19 |
--------------------------------------------------------------------------------
/debug.keystore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jocmp/capyreader/699c32b80716b72a4fdf1dd650cf9f3e7e10f001/debug.keystore
--------------------------------------------------------------------------------
/fastlane/Appfile:
--------------------------------------------------------------------------------
1 | json_key_file('google-play-service-account.json')
2 | package_name("com.capyreader.app")
3 |
--------------------------------------------------------------------------------
/fastlane/Fastfile:
--------------------------------------------------------------------------------
1 | default_platform(:android)
2 |
3 | platform :android do
4 | desc 'Validate Play Store key'
5 | lane :validate_key do
6 | validate_play_store_json_key
7 | end
8 |
9 | desc "Runs all the tests"
10 | lane :test do
11 | gradle(task: "test")
12 | end
13 |
14 | desc "Deploy a new version to the Google Play"
15 | lane :production do
16 | build_release_bundle
17 | upload_to_play_store
18 | end
19 |
20 | lane :github_release do
21 | build_apk_release
22 | end
23 | end
24 |
25 | def build_release_bundle
26 | gradle(task: 'clean bundleGplayRelease')
27 | end
28 |
29 | def build_apk_release
30 | gradle(task: 'clean assembleFreeRelease')
31 | end
32 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1001.txt:
--------------------------------------------------------------------------------
1 | Initial internal testing push
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1002.txt:
--------------------------------------------------------------------------------
1 | Initial Play Store release
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1003.txt:
--------------------------------------------------------------------------------
1 | Behind the scenes tweaks and updates
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1004.txt:
--------------------------------------------------------------------------------
1 | - Add stop button to import notification
2 | - Add option to open links in default browser
3 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1005.txt:
--------------------------------------------------------------------------------
1 | Fixed bug that crashed the app when marking all as read
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1006.txt:
--------------------------------------------------------------------------------
1 | - Add full content support for local feeds
2 | - Add OPML support for imports, not just XML
3 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1007.txt:
--------------------------------------------------------------------------------
1 | Small tweaks to the article card and bugfixes
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1009.txt:
--------------------------------------------------------------------------------
1 | - Fixes import issue for certain RSS feeds
2 | - Adds current version code to the settings menu
3 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1010.txt:
--------------------------------------------------------------------------------
1 | - Adds option to open articles in full content by default
2 | - Adds setting to refresh "On start." The existing "Manually only" now requires a swipe to refresh.
3 | - Fixes a bug that caused the app to crash in the background or when opening a link
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1011.txt:
--------------------------------------------------------------------------------
1 | - Adds swipe navigation to open feed list drawer
2 | - Behind-the-scenes speedup for the article page
3 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1012.txt:
--------------------------------------------------------------------------------
1 | Fixes a blank page bug with articles that were missing outline content
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1013.txt:
--------------------------------------------------------------------------------
1 | - Fix crash for feeds that use incomplete links
2 | - Adds a button to add feeds when starting a new account
3 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1014.txt:
--------------------------------------------------------------------------------
1 | Fix full content parsing for some articles
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1015.txt:
--------------------------------------------------------------------------------
1 | Add options to the article page to change font family and text size.
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1016.txt:
--------------------------------------------------------------------------------
1 | - Tweak subheadings for some article headers
2 | - Fix refresh bug when adding a feed
3 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1017.txt:
--------------------------------------------------------------------------------
1 | Fix crash when adding some feeds manually
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1019.txt:
--------------------------------------------------------------------------------
1 | - Reworks full content mode to remember your last preference for each feed
2 | - Fixes a bug where RSS feeds missing a link could not be added
3 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1020.txt:
--------------------------------------------------------------------------------
1 | Fix issue where sticky full content was not saved after refreshing a feed.
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1021.txt:
--------------------------------------------------------------------------------
1 | • Add full screen view for video in articles
2 | • Clean up Feedbin login page UI
3 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1022.txt:
--------------------------------------------------------------------------------
1 | • Rework Full Content parser to respect autoplay on factorio.com's blog
2 | • Always show video controls when available
3 |
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1023.txt:
--------------------------------------------------------------------------------
1 | • Updates highlight and scrollbar colors on the article page
2 | • Fixes scrolling issue when adding a feed from a long list of feed choices
3 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1024.txt:
--------------------------------------------------------------------------------
1 | Add full-screen article image viewer
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1025.txt:
--------------------------------------------------------------------------------
1 | Add GIF support to article image viewer
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1026.txt:
--------------------------------------------------------------------------------
1 | • Automatically delete read, unstarred articles after 3 months
2 | • Fix image viewer for some feeds
3 |
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1027.txt:
--------------------------------------------------------------------------------
1 | • Add support to play embedded videos
2 | • Add fullscreen image viewer for articles
3 | • Other small UI tweaks to improve colors and controls
4 |
5 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1028.txt:
--------------------------------------------------------------------------------
1 | • Adds settings toggle to disable article auto-delete
2 | • Adds Serbian translations (thank you Whiteowle!)
3 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1029.txt:
--------------------------------------------------------------------------------
1 | • Add per-app language option for system settings
2 | • Fix missing locales for Spanish, Serbian, Portuguese
3 |
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1030.txt:
--------------------------------------------------------------------------------
1 | • Adds settings toggle to disable article auto-delete
2 | • Adds Portuguese, Serbian and Spanish translations thanks to several volunteers
3 | • Fixes auto-delete bug for feeds with old articles
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1031.txt:
--------------------------------------------------------------------------------
1 | • Improve refresh speed when fetching local feeds
2 | • Adds new translations for Czech, German, and Turkish
3 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1032.txt:
--------------------------------------------------------------------------------
1 | • Updates RSS parser to better support non-English characters
2 | • Updates translations, now including Chinese (Simplified)
3 | • Minor UI tweaks to improve padding in settings and the feed edit page
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1033.txt:
--------------------------------------------------------------------------------
1 | • Improve caching for full-screen media
2 | • Fix full content articles for some feeds
3 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1034.txt:
--------------------------------------------------------------------------------
1 | • Add basic support for RDF feeds
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1035.txt:
--------------------------------------------------------------------------------
1 | • Update full content parser to improve accuracy
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1036.txt:
--------------------------------------------------------------------------------
1 | • Add article search
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1037.txt:
--------------------------------------------------------------------------------
1 | Added
2 |
3 | • Article search
4 | • Translations for Chinese thanks to volunteers
5 |
6 | Updated
7 |
8 | • Add support for RDF (RSS 1.0) feeds like Slashdot
9 | • Small UI tweaks in the settings screen and edit feed dialog
10 | • Improve article media loading speed and storage usage
11 | • Translations for Czech, German, Spanish, Italian, Portuguese and Turkish thanks to volunteers
12 |
13 | Fixed
14 |
15 | • Fix bug where Feedbin starred articles were always marked as read on first sync
16 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1038.txt:
--------------------------------------------------------------------------------
1 | Added
2 |
3 | • Translations for Arabic and Italian thanks to volunteers
4 | • Improved transition to article page
5 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1039.txt:
--------------------------------------------------------------------------------
1 | • Rework settings layout
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1040.txt:
--------------------------------------------------------------------------------
1 | Added
2 |
3 | • Added option to change font scale for article list
4 |
5 | Updated
6 |
7 | • Settings page UI
8 | • Translations for Arabic, Italian, Portuguese, Serbian, German, Spanish and Turkish thanks to volunteers
9 | • Improved image caching
10 |
11 | Fixed
12 |
13 | • Refresh bug for some sites
14 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1041.txt:
--------------------------------------------------------------------------------
1 | Added
2 |
3 | • Added option to change font scale for article list
4 |
5 | Updated
6 |
7 | • Settings page UI
8 | • Translations for Arabic, Italian, Portuguese, Serbian, German, Spanish and Turkish thanks to volunteers
9 | • Improved image caching
10 |
11 | Fixed
12 |
13 | • Fixed refresh bug for some sites
14 | • Fixed parsing some non-standard publish dates
15 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1042.txt:
--------------------------------------------------------------------------------
1 | • Add RTL (Right-to-Left) support for the reader page and article list
2 | • Update timestamp UI in article list
3 | • Update Arabic translations thanks to volunteers
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1043.txt:
--------------------------------------------------------------------------------
1 | • Fix position of feed name and date to ensure vertical consistency
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1044.txt:
--------------------------------------------------------------------------------
1 | • Improve spacing in article list row UI
2 | • Export OPML to file instead of share sheet
3 | • Update Chinese (Traditional) translations thanks to volunteers
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1045.txt:
--------------------------------------------------------------------------------
1 | Added
2 |
3 | • Right-to-Left (RTL) support for articles
4 |
5 | Updated
6 |
7 | • Show file explorer when exporting instead of share sheet
8 | • Improve speed of first refresh when adding a feed
9 | • Updated translations for Arabic, Spanish, Italian, Turkish and Chinese thanks to volunteers
10 | • Article list UI with a compact time display
11 |
12 | Fixed
13 |
14 | • Missing unread articles from feeds with old posts
15 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1046.txt:
--------------------------------------------------------------------------------
1 | Added
2 |
3 | • Right-to-Left (RTL) support for articles
4 |
5 | Updated
6 |
7 | • Show file explorer when exporting instead of share sheet
8 | • Improve speed of first refresh when adding a feed
9 | • Updated translations for Arabic, Spanish, Italian, Turkish and Chinese thanks to volunteers
10 | • Article list UI with a compact time display
11 |
12 | Fixed
13 |
14 | • Missing unread articles from feeds with old posts
15 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1047.txt:
--------------------------------------------------------------------------------
1 | • Unpin article top bar on scroll
2 | • Fix timestamps for some feeds
3 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1048.txt:
--------------------------------------------------------------------------------
1 | Added
2 |
3 | • Hide article toolbar on scroll for full screen reading
4 |
5 | Fixed
6 |
7 | • Add support for Brotli encoding to avoid garbled text
8 | • Fix published dates for some feeds
9 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1049.txt:
--------------------------------------------------------------------------------
1 | Added
2 |
3 | • Add option to hide article top bar on scroll for fullscreen reading
4 |
5 | Fixed
6 |
7 | • Add support for Brotli encoding to avoid garbled text
8 | • Fix published dates for some feeds
9 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1050.txt:
--------------------------------------------------------------------------------
1 | Added
2 |
3 | • Swipe gestures to navigate from article to article
4 | • Added button to go to the next article
5 |
6 | Updated
7 |
8 | • Translations for Spanish, Italian and Turkish thanks to volunteers
9 |
10 | Fixed
11 |
12 | • Improved formatting for code blocks in articles
13 | • Fixed crash and improved speed when opening large articles
14 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1051.txt:
--------------------------------------------------------------------------------
1 | Added
2 |
3 | • Swipe gestures to navigate from article to article
4 | • Added button to go to the next article
5 |
6 | Updated
7 |
8 | • Updated Translations for Spanish, Italian and Turkish thanks to volunteers
9 |
10 | Fixed
11 |
12 | • Improved formatting for code blocks in articles
13 | • Fixed crash and improved speed when opening large articles
14 | • Fixed scroll behavior when unpinned
15 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1052.txt:
--------------------------------------------------------------------------------
1 | Added
2 |
3 | • Swipe gestures to navigate from article to article
4 | • Added button to go to the next article
5 |
6 | Updated
7 |
8 | • Updated Translations for Spanish, Italian and Turkish thanks to volunteers
9 |
10 | Fixed
11 |
12 | • Improved formatting for code blocks in articles
13 | • Fixed crash and improved speed when opening large articles
14 | • Fixed scroll behavior when unpinned
15 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1053.txt:
--------------------------------------------------------------------------------
1 | • Add swipe actions to article list items
2 | • Add option to sort unread feeds from oldest to newest
3 | • Fix bug when adding http feeds
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1054.txt:
--------------------------------------------------------------------------------
1 | • Adds back navigation option to open the drawer
2 | • Adds translations for French and Estonian thanks to volunteers
3 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1055.txt:
--------------------------------------------------------------------------------
1 | Added
2 |
3 | • Add swipe actions to article list items
4 | • Adds back navigation option to open the drawer
5 | • Add option to sort unread feeds from oldest to newest
6 | • Add a scrollbar to the article list
7 | • Adds translations for French and Estonian thanks to volunteers
8 |
9 | Updated
10 |
11 | • Updates translations for Serbian, German, Spanish, Italian, Turkish and Chinese thanks to volunteers
12 |
13 | Fixed
14 |
15 | • Fix bug when adding http feeds
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1056.txt:
--------------------------------------------------------------------------------
1 | • Add initial JSON Feed support
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1057.txt:
--------------------------------------------------------------------------------
1 | • Improve reader loading speed
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1058.txt:
--------------------------------------------------------------------------------
1 | Added
2 |
3 | • Added support for JSON feeds. Learn more at https://www.jsonfeed.org/version/1.1/.
4 |
5 | Updated
6 |
7 | • Improve reader speed when opening articles
8 | • Updates translations for Spanish, Estonian, Indonesian, Italian, Turkish and Chinese thanks to volunteers
9 |
10 | Fixed
11 |
12 | • Handle date-only published dates formatted like "Sep 20, 2024" for local feeds
13 | • Fix bug when marking all articles as read offline
14 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1059.txt:
--------------------------------------------------------------------------------
1 | A small update with a handful of improvements
2 |
3 | • Update external libraries to their latest versions
4 | • Fix networking crash for Feedbin on bad networks
5 | • Fix layout bug that occurs when app restarts after an article is selected
6 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1060.txt:
--------------------------------------------------------------------------------
1 | • Added feed notification support to track when new articles are published
2 | • Added Bulgarian as a language option with updates to existing language options thanks to volunteers.
3 | • Fixed a bug where the app would crash when returning to settings
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1061.txt:
--------------------------------------------------------------------------------
1 | Added
2 |
3 | • Added a swipe option to open links in browser
4 |
5 | Updated
6 |
7 | • Updated translations including new options for Bulgarian and Russian thanks to volunteers.
8 |
9 | Fixed
10 |
11 | • Improved sync speed for Feedbin accounts with many feeds
12 | • Fixed a bug that chose the reverse order when marking entries read
13 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1062.txt:
--------------------------------------------------------------------------------
1 | Added
2 |
3 | • Added article notifications
4 | • Added a swipe option to open links in browser
5 |
6 | Updated
7 |
8 | • Updated translations including new options for Bulgarian and Russian thanks to volunteers.
9 |
10 | Fixed
11 |
12 | • Improved sync speed for Feedbin accounts with many feeds
13 | • Fixed a bug that chose the reverse order when marking entries read
14 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1063.txt:
--------------------------------------------------------------------------------
1 | Added
2 |
3 | • Added article notifications
4 | • Added a swipe option to open links in browser
5 |
6 | Updated
7 |
8 | • Updated translations including new options for Bulgarian and Russian thanks to volunteers.
9 |
10 | Fixed
11 |
12 | • Improved sync speed for Feedbin accounts with many feeds
13 | • Fixed a bug that chose the reverse order when marking entries read
14 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1064.txt:
--------------------------------------------------------------------------------
1 | • Update notifications to include full article title
2 | • Fix link enclosure handling in Atom feeds
3 | • Add option to disable "Mark all as read" confirmation dialog
4 | • Update translations for Arabic, Turkish, Chinese, Italian, French, Estonian and Bulgarian thanks to volunteers
5 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1065.txt:
--------------------------------------------------------------------------------
1 | • Add star and read options to long-press list menu
2 | • Fix list swipe gesture detection
3 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1066.txt:
--------------------------------------------------------------------------------
1 | Added
2 |
3 | • Add option to disable "Mark all as read" confirmation dialog
4 | • Add star and read options to long-press list menu
5 |
6 | Updated
7 |
8 | • Update translations for Arabic, Turkish, Chinese, Italian, French, Estonian and Bulgarian thanks to volunteers
9 |
10 | Fixed
11 |
12 | • Fix list swipe gesture detection
13 | • Update notifications to include full article title
14 | • Fix link enclosure handling in Atom feeds
15 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1067.txt:
--------------------------------------------------------------------------------
1 | Added
2 |
3 | • Add option to disable "Mark all as read" confirmation dialog
4 | • Add star and read options to long-press list menu
5 |
6 | Updated
7 |
8 | • Update translations for Arabic, Turkish, Chinese, Italian, French, Estonian and Bulgarian thanks to volunteers
9 |
10 | Fixed
11 |
12 | • Fix list swipe gesture detection
13 | • Update notifications to include full article title
14 | • Fix link enclosure handling in Atom feeds
15 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1068.txt:
--------------------------------------------------------------------------------
1 | • Open article notifications to article on the "Unread" filter instead of the "All" filter.
2 | • Fix scrollbar padding in reader
3 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1069.txt:
--------------------------------------------------------------------------------
1 | • Fix handling for feeds that are missing article URL
2 | • Fix swipe gesture padding when toolbars are pinned
3 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1070.txt:
--------------------------------------------------------------------------------
1 | Updated
2 |
3 | • Updated translations for Serbian, Bulgarian, Czech, Spanish, and Chinese thanks to volunteers
4 |
5 | Fixed
6 |
7 | • Fixed padding on scrollbar in long articles
8 | • Fix handling for feeds that are missing article URL
9 | • Added more error handling around article refresh on spotty networks
10 |
11 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1071.txt:
--------------------------------------------------------------------------------
1 | Added
2 |
3 | • Add initial support for FreshRSS and Google Reader API
4 |
5 | Fixed
6 |
7 | • Fixed missing images on some articles
8 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1072.txt:
--------------------------------------------------------------------------------
1 | Added
2 |
3 | • Add initial support for FreshRSS and Google Reader API
4 |
5 | Updated
6 |
7 | • Updated translations for Arabic, French, Russian, and Chinese thanks to volunteers
8 |
9 | Fixed
10 |
11 | • Fixed missing images on some articles
12 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1073.txt:
--------------------------------------------------------------------------------
1 | Added
2 |
3 | • Add initial support for FreshRSS and Google Reader API
4 |
5 | Updated
6 |
7 | • Updated translations for Arabic, Bulgarian, German, Estonian, French, Italian, Russian, and Chinese thanks to volunteers
8 |
9 | Fixed
10 |
11 | • Fixed missing images on some articles
12 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1074.txt:
--------------------------------------------------------------------------------
1 | Added
2 |
3 | • Added "Tap to page article" gesture option for navigation on E-Ink readers
4 |
5 | Updated
6 |
7 | • Updated German, Spanish and Indonesian translations thanks to volunteers
8 |
9 | Fixed
10 |
11 | • Fix missing User-Agent headers when requesting images
12 | • Ensure that invalid favicons are not requested between feed refreshes
13 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1075.txt:
--------------------------------------------------------------------------------
1 | Added
2 |
3 | • Show alt text with their images in the media viewer
4 |
5 | Updated
6 |
7 | • Updated translations for Spanish, Chinese, French, Estonian, and Bulgarian thanks to volunteers
8 | • New font options including Jost, Inter, and Literata
9 |
10 | Fixed
11 |
12 | • Re-added handling for images with links so that they are always shown in the media viewer
13 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1076.txt:
--------------------------------------------------------------------------------
1 | Added
2 |
3 | • Added high contrast dark theme meant for OLED displays
4 |
5 | Updated
6 |
7 | • Updated translations for Bulgarian, Estonian, Italian, and Chinese thanks to volunteers
8 |
9 | Fixed
10 |
11 | • Fixed formatting for code blocks in HTML tables
12 | • Fixed article preview images for feeds with gravatars
13 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1077.txt:
--------------------------------------------------------------------------------
1 | Added
2 |
3 | • Added "Tap to page article" gesture option for E-Ink readers
4 | • Show alt text with their images in the media viewer
5 | • Added high contrast dark theme
6 |
7 | Updated
8 |
9 | • Added new font options Jost, Inter, and Literata
10 | • Updated Bulgarian, Estonian, German, Spanish, French, Italian, Indonesian, Chinese translations thanks to volunteers
11 |
12 | Fixed
13 |
14 | • Feed fixes around favicons and preview images
15 | • Fixed formatting for code blocks in HTML tables
16 | • Fixed article image bug causing app crashes
17 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1078.txt:
--------------------------------------------------------------------------------
1 | • Added option to mark read on scroll
2 | • Added option to hide images in articles
3 | • Updated translations for Spanish, Bulgarian, Norwegian, Italian, Chinese, German and Estonian thanks to volunteers
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1079.txt:
--------------------------------------------------------------------------------
1 | ∙ Added support to fetch read articles from FreshRSS
2 | ∙ Updated translations for Arabic, Bulgarian, German, Spanish, Estonian, Italian, Russian, and Chinese thanks to volunteers
3 | ∙ Updated the full content parser to better support lead images and custom parsing
4 | ∙ Fixed scroll position bug when switching between feeds
5 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1080.txt:
--------------------------------------------------------------------------------
1 | ∙ Added support to fetch read articles from FreshRSS
2 | ∙ Updated translations for Arabic, Bulgarian, German, Spanish, Estonian, Italian, Russian, and Chinese thanks to volunteers
3 | ∙ Updated the full content parser to better support lead images and custom parsing
4 | ∙ Fixed scroll position bug when switching between feeds
5 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1081.txt:
--------------------------------------------------------------------------------
1 | • Added option for single-column layouts on big screens
2 | • Fixed bug where sticky content preference didn't reset after toggle
3 | • Use device-local article parser for Feedbin
4 | • Updated translations for Bulgarian, Norwegian, and Italian thanks to volunteers
5 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1082.txt:
--------------------------------------------------------------------------------
1 | • Add end-of-feed swipe gesture to move to the next feed
2 | • Add transition to move next feed when marking all as read
3 | • Improve FreshRSS login wording
4 | • Update translations for Spanish, Chinese, Italian, Estonian, Bulgarian, German, and Serbian thanks to volunteers
5 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1083.txt:
--------------------------------------------------------------------------------
1 | Added
2 |
3 | • Option to mark read on scroll
4 | • Option to hide images in articles
5 | • Option to move next feed when marking all as read
6 | • Option for single-column layouts on big screens
7 | • End-of-feed swipe gesture to move to the next feed
8 |
9 | Updated
10 |
11 | • Updated translations for Arabic, Bulgarian, Chinese, Estonian, German, Italian, Tamil, Norwegian, Russian, Serbian, and Spanish thanks to volunteers
12 |
13 | Fixed
14 |
15 | • Improve FreshRSS login wording
16 | • Other small bugfixes
17 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1084.txt:
--------------------------------------------------------------------------------
1 | • Adds Galician and Polish translations to the app. Thanks!
2 | • Adds end-of-list behavior when marking items as read on scroll
3 | • Allow adding empty feeds
4 | • Updated translations for Bulgarian, Estonian, Russian and Chinese thanks to volunteers
5 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1085.txt:
--------------------------------------------------------------------------------
1 | • Update article parser for some sites
2 | • Fixed an issue to ensure YouTube videos don't autoplay on swipe
3 | • Update translations for Italian (thanks!)
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1086.txt:
--------------------------------------------------------------------------------
1 | • Adds end-of-list behavior when marking items as read on scroll
2 | • Allow adding empty local feeds like Google Alerts
3 | • Adds Galician, Polish and Malayalam translations to the app to volunteers
4 | • Updated custom parser for some sites
5 | • Updated styling for YouTube thumbnails
6 | • Updated translations for Bulgarian, Spanish, Estonian, French, Italian, Russian and Chinese thanks to volunteers
7 | • Fixed timestamps for local feeds for some locales
8 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1087.txt:
--------------------------------------------------------------------------------
1 | Small bugfix update!
2 |
3 | • Allow user-provided network certificates
4 | • Adds translations for Welsh thanks to a volunteer
5 | • Updated translations for Polish, French, Portuguese, Czech and German thanks to volunteers.
6 | • Improve favicon fallback handling
7 | • Improved client-local crash reporting
8 | • Fixed a bug cross-site origin bug when parsing some feeds
9 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1088.txt:
--------------------------------------------------------------------------------
1 | • Add better local discovery for known websites like YouTube and Reddit
2 | • Rework notifications to display in a single dropdown
3 | • Combine lists in single column reader mode
4 | • Update FreshRSS read status on fetch
5 | • Small behavior tweaks to the feed list
6 | • Fix Cyrillic charset bug for local feeds
7 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1089.txt:
--------------------------------------------------------------------------------
1 | Added
2 |
3 | • Add better local discovery for known websites like YouTube and Reddit
4 | • Add more behavior options to Mark All Read
5 |
6 | Updated
7 |
8 | • Add error indicator when full content fails to load
9 | • Rework notifications to display in a single dropdown
10 | • Combine lists in single column reader mode on tablets
11 |
12 | Fixed
13 |
14 | • Fix article link in reader heading for some feeds
15 | • Fix Cyrillic character set bug for local feeds
16 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1090.txt:
--------------------------------------------------------------------------------
1 | • Add custom full content parser for techcrunch.com
2 | • Improve FreshRSS image preview parsing
3 | • Update translations for Bulgarian, Estonian, Galician, Portuguese, and Chinese thanks to volunteers.
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1091.txt:
--------------------------------------------------------------------------------
1 | • Add more font sizes to article list and reader options
2 | • Updated translations for Bulgarian, Chinese, Estonian, French, Galician, German, Italian, Portuguese and Russian thanks to volunteers.
3 | • Ensure parser handles accented characters for some feed encodings
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1092.txt:
--------------------------------------------------------------------------------
1 | • Simplify Capy Reader's user-agent string to avoid getting caught by bad bot detection policies.
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1093.txt:
--------------------------------------------------------------------------------
1 | • FreshRSS will now sync individual feeds and folders for their entire history. Top-level sync stays the same by refreshing all new entries.
2 | • Show absolute time and date in article feed
3 | • Updated translations for Chinese and Galician thanks to volunteers
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1094.txt:
--------------------------------------------------------------------------------
1 | Added
2 |
3 | • Add custom full content parser for techcrunch.com
4 | • Add more font sizes to article list and reader options
5 |
6 | Updated
7 |
8 | • Improve FreshRSS image previews
9 | • Improve FreshRSS refresh for old articles
10 | • Show absolute time and date in article feed
11 | • Automatically dismiss notifications when read
12 | • Updated translations thanks to volunteers.
13 |
14 | Fixed
15 |
16 | • Fix bug for accented characters for some feeds
17 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1095.txt:
--------------------------------------------------------------------------------
1 | • Fix bug where notification titles appear incorrectly for local feeds
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1096.txt:
--------------------------------------------------------------------------------
1 | • Add "Copy Link" menu item to article list menu
2 | • Improve image caption display
3 | • Improve custom extractor for versants.com and androidauthority.com
4 | • Autofill "/api/greader.php" path when logging into FreshRSS
5 | • Update translations for Arabic thanks to volunteers.
6 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1097.txt:
--------------------------------------------------------------------------------
1 | • Add "Copy Link" menu item to article list menu
2 | • Autofill "/api/greader.php" path when logging into FreshRSS
3 | • Improve image caption display
4 | • Improve custom extractors for several sites
5 | • Update translations for Arabic, Chinese, Czech, Galacian, Estonian, German, Italian, Indonesian, Nepali, Russian, Swedish thanks to volunteers.
6 | • Fix HTML in titles for local feeds
7 | • Fix imports for some local feeds
8 | • Fix category refresh for FreshRSS feeds
9 | • Fix favicon icons that use SVG
10 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1098.txt:
--------------------------------------------------------------------------------
1 | • Fix error when adding feeds with bad favicon
2 | • Fix incorrect styling for articles with multiple YouTube embeds
3 | • Make article status filters more visible and easier to reach
4 | • Update translations for Bulgarian, Indonesian, Nepali, Polish and Swedish thanks to volunteers.
5 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1099.txt:
--------------------------------------------------------------------------------
1 | • Fix local feed persistence bug where item URLs were not unique
2 | • Fix custom extractor for androidauthority.com
3 | • Add initial support to fetch FreshRSS user labels
4 | • Update translations for Bulgarian, Czech, Estonian, French, Galician, Italian, Nepali, Swedish, and Chinese thanks to volunteers.
5 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1100.txt:
--------------------------------------------------------------------------------
1 | • Horizontal navigation in the reader! Swipe left to right to navigate between articles.
2 | • Update translations for Bulgarian, Spanish, Estonian, Russian, and Chinese thanks to volunteers.
3 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1101.txt:
--------------------------------------------------------------------------------
1 | Added
2 |
3 | • Horizontal navigation in the reader!
4 | • Added initial support to fetch FreshRSS user labels
5 |
6 | Updated
7 |
8 | • Make article status filters more visible and easier to reach
9 | • Improved performance when fetching individual local feeds and folders
10 | • Update translations thanks to volunteers
11 | • Updated custom content extractors for various sites
12 |
13 | Fixed
14 |
15 | • Fix visual bugs in articles with YouTube embeds
16 | • Fix local feed persistence bug where item URLs were not unique
17 | • Even more bug fixes
18 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1102.txt:
--------------------------------------------------------------------------------
1 | • Fall back to summary in notifications if title is missing
2 | • Hide title in list view when title is missing
3 | • Attempt to fix clipped articles by reconfiguring the reader view
4 | • Fix blockquote size inside figures
5 | • Updated custom parser for bsky.app and se.pl
6 | • Only autoscroll list view when necessary (thanks to a volunteer!)
7 | • Update translations for Italian and French thanks to volunteers
8 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1103.txt:
--------------------------------------------------------------------------------
1 | Bugfix release:
2 |
3 | • Add fallback behavior for article titles
4 | • Only autoscroll list view when necessary
5 | • Updated custom parsers for bsky.app and se.pl
6 | • Fix clipped articles when paging
7 | • Make horizontal scroll optional
8 | • Fix article selection when app is reopened
9 | • Display URL if title and summary are empty
10 | • Skip already created notifications on notify
11 | • Deduplicate feeds in notification settings
12 | • Update translations for Italian, French, Russian, and Swedish thanks to volunteers
13 | • And more!
14 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1104.txt:
--------------------------------------------------------------------------------
1 | • Fix horizontal scrolling jank and speed up opening reader view
2 | • Ensure articles are not closed when unstarred
3 | • Update custom parsers for arstechnica.com and se.pl
4 | • Ensure all feeds are refreshed on start
5 | • Update translations for Bulgarian, Czech, Estonian, French, Galician, Italian, Portuguese, and Chinese thanks to volunteers!
6 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1105.txt:
--------------------------------------------------------------------------------
1 | Bugfix release. New features are in the works soon!
2 |
3 | • Fix horizontal scrolling jank
4 | • Speed up reader when opened
5 | • Ensure articles are not closed when unstarred
6 | • Update custom parsers for arstechnica.com and se.pl
7 | • Ensure all feeds are refreshed on start
8 | • Update translations for Bulgarian, Czech, Estonian, French, Galician, Italian, Portuguese, and Chinese thanks to volunteers
9 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1106.txt:
--------------------------------------------------------------------------------
1 | • Add button to navigation drawer to refresh all feeds
2 | • Add save and share buttons to the article media viewer
3 | • Add "Mark as read" button to article notifications
4 | • Add option to disable bottom swipe between feeds
5 | • Persist folder dropdown expanded state
6 | • Fix local feed articles that use blank GUIDs
7 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1107.txt:
--------------------------------------------------------------------------------
1 | • Tweak wait time when marking read as scroll
2 | • Display saved search collections for Feedbin
3 | • Update Mercury Parser for some feeds
4 | • Update error message for adding a feed when offline
5 | • Update translations for Arabic, Bulgarian, Estonian, French, Galician, Italian and Chinese thanks to volunteers.
6 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1108.txt:
--------------------------------------------------------------------------------
1 | Added
2 |
3 | • Add button to navigation drawer to refresh all feeds
4 | • Add save and share buttons to the article media viewer
5 | • Add "Mark as read" button to notifications
6 | • Display saved search collections for Feedbin
7 |
8 | Updated
9 |
10 | • Persist folder dropdown expanded state
11 | • Update translations for Arabic, Bulgarian, Estonian, French, Galician, Italian, Polish and Chinese thanks to volunteers.
12 |
13 | Fixed
14 |
15 | • Tweak wait time when marking read as scroll
16 | • Handle invalid content type for local feeds
17 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1109.txt:
--------------------------------------------------------------------------------
1 | • Add "Load full content" as a swipe up option
2 | • Update Mercury Parser for lebensmittelwarnung.de
3 | • Fix handling for dates in some feeds
4 | • Fix image cleaner for non-standard attributes
5 | • Undo timing change for "Mark as read on scroll"
6 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1110.txt:
--------------------------------------------------------------------------------
1 | - Fix database compatibility bug on Android 13 and below
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1111.txt:
--------------------------------------------------------------------------------
1 | • Add "Load full content" as a swipe up option in the reader
2 | • Add medium image option for article list
3 | • Undo timing change for "Mark as read on scroll"
4 | • Update article parser for several sites
5 | • Fix handling for dates in some feeds
6 | • Fix image cleaner for non-standard attributes
7 | • Fix database compatibility bug on Android 13 and below
8 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1112.txt:
--------------------------------------------------------------------------------
1 | • Add full-content parser for 9to5google.com
2 | • Improve parsing for YouTube on locale feeds
3 | • Improve notification cleanup on "Mark as read"
4 | • Update translations for Polish and Russian thanks to volunteers
5 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1113.txt:
--------------------------------------------------------------------------------
1 | • Improve drawer refresh button animation
2 | • Fix letter casing bug when editing multiple tags with the same name
3 | • Update translations for Bulgarian, Spanish, Estonian, French, Galician, Italian, Polish and Chinese thanks to volunteers.
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1114.txt:
--------------------------------------------------------------------------------
1 | • Add full-content parser for 9to5google.com
2 | • Update parsing for YouTube on local feeds
3 | • Update drawer refresh button animation
4 | • Update translations for Bulgarian, Czech, Spanish, Estonian, French, Galician, Italian, Polish, Russian and Chinese thanks to volunteers.
5 | • Improve notification cleanup on "Mark as read"
6 | • Fix letter casing bug when editing multiple tags with the same name
7 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1115.txt:
--------------------------------------------------------------------------------
1 | • Add ability to add a feed from other apps using the share sheet
2 | • Update translations for Italian and Russian thanks to volunteers
3 | • Update custom parsers for engadget.com and tarnkappe.info
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1116.txt:
--------------------------------------------------------------------------------
1 | • Add dropdowns for tags, saved searches, and top-level feeds
2 | • Add ability to block keywords for local accounts under General settings
3 | • Updated translations for Arabic, Latvian, and German thanks to volunteers
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1117.txt:
--------------------------------------------------------------------------------
1 | Added
2 |
3 | • Add a feed from other apps via a new share sheet icon
4 | • Add Latvian as a translation option thanks to a volunteer!
5 | • Add dropdowns for tags, saved searches, and top-level feeds
6 | • Add ability to block keywords for local accounts under General settings
7 |
8 | Updated
9 |
10 | • Update custom parsers for engadget.com and tarnkappe.info
11 | • Update translations for Arabic, Bulgarian, German, French, Galician, Italian, and Russian thanks to volunteers
12 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1118.txt:
--------------------------------------------------------------------------------
1 | • Make tables in articles full width to avoid clipping
2 | • Fix selection bug when reopening a previously selected feed
3 | • Fix Miniflux bug where read articles are marked as unread when reselected
4 | • Update translations for Spanish, Estonian, French, Italian, Latvian, Polish and Chinese thanks to volunteers.
5 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1119.txt:
--------------------------------------------------------------------------------
1 | • Add menu options to rename or delete tags
2 | • [FreshRSS] Reword "Add or Select Tag" field when editing a feed
3 | • Update translations for French and Latvian thanks to volunteers
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1120.txt:
--------------------------------------------------------------------------------
1 | • Add menu options to rename or delete tags
2 | • Add long press options when selecting article links
3 | • Add "Navigate to Parent" back nav option
4 | • Update translations for Bulgarian, German, Estonian, French, Galician, Italian, and Latvian, Polish, Russian and Chinese thanks to volunteers
5 | • Reword "Add or Select Tag" field when editing a FreshRSS feed
6 | • Ensure animated GIFs are shown in reader
7 | • Fix "author" tag parsing for local RSS feeds
8 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1121.txt:
--------------------------------------------------------------------------------
1 | Small bugfix release!
2 |
3 | • Fix bug where article text couldn't be highlighted on long press
4 | • Update translations for Bulgarian, Galician, Italian, and Chinese thanks to volunteers.
5 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1122.txt:
--------------------------------------------------------------------------------
1 | • Add custom parser for vortez.net
2 | • Set max lines to 3 for article list titles
3 | • Add font size options in reader view
4 | • Update translations for Bulgarian and German thanks to volunteers
5 | • Fix Miniflux crash when adding a new feed
6 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1123.txt:
--------------------------------------------------------------------------------
1 | • Add custom parser for vortez.net
2 | • Set max lines to 3 for article list titles
3 | • Add font size options in reader view
4 | • Add new translations for Portuguese thanks to a volunteer!
5 | • Update translations for Bulgarian, German, and Russian thanks to volunteers
6 | • Fix Miniflux crash when adding a new feed
7 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1124.txt:
--------------------------------------------------------------------------------
1 | • Add latest headlines widget to display unread articles
2 | • Fix crash when marking 1,000 or more articles as read
3 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1125.txt:
--------------------------------------------------------------------------------
1 | • Add latest headlines widget to display unread articles
2 | • Update translations for Bulgarian, German, Spanish, French, Galician, Italian, Polish, Turkish and Chinese thanks to volunteers
3 | • Fix crash when marking 1,000 or more articles as read
4 | • Fix missing styling on video embeds
5 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1126.txt:
--------------------------------------------------------------------------------
1 | • Add option to disable some gestures for better TalkBack support
2 | • Show feed title if article title is missing in reader
3 | • [Local] Add better support for date-only timestamps
4 | • Update translations for Estonian, Latvian, Russian and Turkish thanks to volunteers
5 | • Ensure notifications open to unread filter
6 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1127.txt:
--------------------------------------------------------------------------------
1 | • Fix empty reader view on TalkBack setting
2 | • Update translations for Bulgarian, German, Estonian, French, Galician, Turkish, and Chinese
3 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1128.txt:
--------------------------------------------------------------------------------
1 | • [Local] Fix rich text on feeds like the GrapheneOS release notes
2 | • Move article actions to a Material bottom bar for easier access
3 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1129.txt:
--------------------------------------------------------------------------------
1 | • Add better support for date-only timestamps on local accounts
2 | • Add option to show article actions on a bottom bar for easier access
3 | • Add option to disable some gestures for better TalkBack support
4 | • Ensure notifications open to unread filter
5 | • Update translations for Arabic, Bulgarian, German, Estonian, Spanish, French, Galician, Italian, Latvian, Polish, Russian, Turkish, and Chinese thanks to volunteers
6 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1130.txt:
--------------------------------------------------------------------------------
1 | • Fix parsing for local Atom feeds
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1131.txt:
--------------------------------------------------------------------------------
1 | • Add support for FreshRSS image enclosures
2 | • Mark articles as on swipe-to-open from list
3 | • Fix feed selection when app is refreshing
4 | • Prevent "mark all read" from marking entire list as read on search filter
5 | • Update translations for Bulgarian, German, Spanish, Estonian, French, Galician, Italian, Polish, Tamil, and Chinese thanks to volunteers
6 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1132.txt:
--------------------------------------------------------------------------------
1 | • Add support for FreshRSS image enclosures
2 | • Mark articles as on swipe-to-open from list
3 | • Update custom parsers for polygon.com and theverge.com
4 | • Fix feed selection when app is refreshing
5 | • Prevent "mark all read" from marking entire list as read on search filter
6 | • Update translations for Bulgarian, German, Spanish, Estonian, French, Galician, Italian, Indonesian, Latvian, Polish, Tamil, Turkish, and Chinese thanks to volunteers
7 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1133.txt:
--------------------------------------------------------------------------------
1 | • Fix Feedbin refresh bug caused by parsing issues
2 | • Update translations for Russian and Latvian thanks to volunteers
3 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1134.txt:
--------------------------------------------------------------------------------
1 | • Add support for image enclosures on local accounts
2 | • Update dependencies
3 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1135.txt:
--------------------------------------------------------------------------------
1 | • Add support for image enclosures on local accounts
2 | • Add custom parser for techpowerup.com
3 | • Ensure tags are alphabetically ordered in the edit feed dialog
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1136.txt:
--------------------------------------------------------------------------------
1 | • Move "Mark all as read" to a floating action button
2 | • Open notifications to the last filter used instead of "Unread"
3 | • Update translations thanks to volunteers
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1137.txt:
--------------------------------------------------------------------------------
1 | • Update Greek translations thanks to a volunteer!
2 | • Remove crossfade animation to avoid jutter when switching feeds
3 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1138.txt:
--------------------------------------------------------------------------------
1 | • Move "Mark all as read" to a floating action button
2 | • Remove crossfade animation to avoid jutter when switching feeds
3 | • Open notifications to the last filter used instead of "Unread"
4 | • Update translations thanks to volunteers
5 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1139.txt:
--------------------------------------------------------------------------------
1 | • Fix feed name ordering for titles with accented characters
2 | • Ensure Feedbin saved searches articles are always refreshed
3 | • Update translations for Arabic, Bulgarian, German, Greek, Estonian, French, Galician, Italian, Malayalam, Polish, and Chinese thanks to volunteers.
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/full_description.txt:
--------------------------------------------------------------------------------
1 | Just like its namesake, Capy Reader is a small RSS reader focused on approachability and simplicity.
2 |
3 | Some features include:
4 |
5 | • Syncing via Feedbin, FreshRSS, or directly to your device
6 | • Track read and starred articles
7 | • Article search
8 | • Headlines widget
9 | • Notifications
10 | • Full content mode
11 | • Dark mode
12 | • Big screen support
13 | • Import and export your saved feeds
14 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/featureGraphic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jocmp/capyreader/699c32b80716b72a4fdf1dd650cf9f3e7e10f001/fastlane/metadata/android/en-US/images/featureGraphic.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jocmp/capyreader/699c32b80716b72a4fdf1dd650cf9f3e7e10f001/fastlane/metadata/android/en-US/images/icon.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/dark_article.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jocmp/capyreader/699c32b80716b72a4fdf1dd650cf9f3e7e10f001/fastlane/metadata/android/en-US/images/phoneScreenshots/dark_article.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/dark_list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jocmp/capyreader/699c32b80716b72a4fdf1dd650cf9f3e7e10f001/fastlane/metadata/android/en-US/images/phoneScreenshots/dark_list.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/light_article.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jocmp/capyreader/699c32b80716b72a4fdf1dd650cf9f3e7e10f001/fastlane/metadata/android/en-US/images/phoneScreenshots/light_article.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/light_list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jocmp/capyreader/699c32b80716b72a4fdf1dd650cf9f3e7e10f001/fastlane/metadata/android/en-US/images/phoneScreenshots/light_list.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/sevenInchScreenshots/dark_article.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jocmp/capyreader/699c32b80716b72a4fdf1dd650cf9f3e7e10f001/fastlane/metadata/android/en-US/images/sevenInchScreenshots/dark_article.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/sevenInchScreenshots/light_article.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jocmp/capyreader/699c32b80716b72a4fdf1dd650cf9f3e7e10f001/fastlane/metadata/android/en-US/images/sevenInchScreenshots/light_article.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/tenInchScreenshots/dark_article.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jocmp/capyreader/699c32b80716b72a4fdf1dd650cf9f3e7e10f001/fastlane/metadata/android/en-US/images/tenInchScreenshots/dark_article.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/tenInchScreenshots/light_article.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jocmp/capyreader/699c32b80716b72a4fdf1dd650cf9f3e7e10f001/fastlane/metadata/android/en-US/images/tenInchScreenshots/light_article.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/tenInchScreenshots/light_list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jocmp/capyreader/699c32b80716b72a4fdf1dd650cf9f3e7e10f001/fastlane/metadata/android/en-US/images/tenInchScreenshots/light_list.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/short_description.txt:
--------------------------------------------------------------------------------
1 | A smallish RSS reader
2 |
--------------------------------------------------------------------------------
/feedbinclient/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/feedbinclient/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("java-library")
3 | id("org.jetbrains.kotlin.jvm")
4 | id("com.google.devtools.ksp") version libs.versions.ksp
5 | }
6 |
7 | java {
8 | sourceCompatibility = JavaVersion.VERSION_21
9 | targetCompatibility = JavaVersion.VERSION_21
10 | }
11 |
12 | dependencies {
13 | implementation(libs.kotlinx.coroutines.core)
14 | implementation(libs.moshi)
15 | implementation(libs.moshi.converter)
16 | implementation(libs.retrofit2.retrofit)
17 | implementation(libs.retrofit2.retrofit)
18 | ksp(libs.moshi.kotlin.codegen)
19 | testImplementation(kotlin("test"))
20 | testImplementation(libs.tests.junit)
21 | testImplementation(libs.tests.kotlinx.coroutines)
22 | testImplementation(libs.tests.mockk.mockk)
23 | }
24 |
--------------------------------------------------------------------------------
/feedbinclient/src/main/java/com/jocmp/feedbinclient/CreateSubscriptionRequest.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.feedbinclient
2 |
3 | import com.squareup.moshi.JsonClass
4 |
5 | @JsonClass(generateAdapter = true)
6 | data class CreateSubscriptionRequest(
7 | val feed_url: String
8 | )
9 |
--------------------------------------------------------------------------------
/feedbinclient/src/main/java/com/jocmp/feedbinclient/CreateTaggingRequest.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.feedbinclient
2 |
3 | import com.squareup.moshi.JsonClass
4 |
5 | @JsonClass(generateAdapter = true)
6 | data class CreateTaggingRequest(
7 | val feed_id: String,
8 | val name: String,
9 | )
10 |
--------------------------------------------------------------------------------
/feedbinclient/src/main/java/com/jocmp/feedbinclient/DeleteTagRequest.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.feedbinclient
2 |
3 | import com.squareup.moshi.JsonClass
4 |
5 | @JsonClass(generateAdapter = true)
6 | data class DeleteTagRequest(val name: String)
7 |
--------------------------------------------------------------------------------
/feedbinclient/src/main/java/com/jocmp/feedbinclient/Enclosure.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.feedbinclient
2 |
3 | import com.squareup.moshi.JsonClass
4 |
5 | @JsonClass(generateAdapter = true)
6 | data class Enclosure(
7 | val enclosure_url: String,
8 | val enclosure_type: String,
9 | val enclosure_length: String?,
10 | val itunes_duration: String?,
11 | val itunes_image: String?,
12 | )
13 |
--------------------------------------------------------------------------------
/feedbinclient/src/main/java/com/jocmp/feedbinclient/ExtractedContent.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.feedbinclient
2 |
3 | import com.squareup.moshi.JsonClass
4 |
5 | @JsonClass(generateAdapter = true)
6 | data class ExtractedContent(
7 | val content: String
8 | )
9 |
--------------------------------------------------------------------------------
/feedbinclient/src/main/java/com/jocmp/feedbinclient/Icon.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.feedbinclient
2 |
3 | import com.squareup.moshi.JsonClass
4 |
5 | @JsonClass(generateAdapter = true)
6 | data class Icon(
7 | val host: String,
8 | val url: String,
9 | )
10 |
--------------------------------------------------------------------------------
/feedbinclient/src/main/java/com/jocmp/feedbinclient/SavedSearch.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.feedbinclient
2 |
3 | import com.squareup.moshi.JsonClass
4 |
5 | @JsonClass(generateAdapter = true)
6 | data class SavedSearch(
7 | val id: Long,
8 | val name: String,
9 | val query: String,
10 | )
11 |
--------------------------------------------------------------------------------
/feedbinclient/src/main/java/com/jocmp/feedbinclient/StarredEntriesRequest.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.feedbinclient
2 |
3 | import com.squareup.moshi.JsonClass
4 |
5 | @JsonClass(generateAdapter = true)
6 | data class StarredEntriesRequest(
7 | val starred_entries: List,
8 | )
9 |
--------------------------------------------------------------------------------
/feedbinclient/src/main/java/com/jocmp/feedbinclient/Subscription.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.feedbinclient
2 |
3 | import com.squareup.moshi.JsonClass
4 |
5 | @JsonClass(generateAdapter = true)
6 | data class Subscription(
7 | val id: Long,
8 | val created_at: String,
9 | val feed_id: Long,
10 | val title: String,
11 | val feed_url: String,
12 | val site_url: String
13 | )
14 |
--------------------------------------------------------------------------------
/feedbinclient/src/main/java/com/jocmp/feedbinclient/Tagging.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.feedbinclient
2 |
3 | import com.squareup.moshi.JsonClass
4 |
5 | @JsonClass(generateAdapter = true)
6 | data class Tagging(
7 | val id: Long,
8 | val feed_id: Long,
9 | val name: String
10 | )
11 |
--------------------------------------------------------------------------------
/feedbinclient/src/main/java/com/jocmp/feedbinclient/UnreadEntriesRequest.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.feedbinclient
2 |
3 | import com.squareup.moshi.JsonClass
4 |
5 | @JsonClass(generateAdapter = true)
6 | data class UnreadEntriesRequest(
7 | val unread_entries: List,
8 | )
9 |
--------------------------------------------------------------------------------
/feedbinclient/src/main/java/com/jocmp/feedbinclient/UpdateSubscriptionRequest.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.feedbinclient
2 |
3 | import com.squareup.moshi.JsonClass
4 |
5 | @JsonClass(generateAdapter = true)
6 | data class UpdateSubscriptionRequest(
7 | val title: String
8 | )
9 |
--------------------------------------------------------------------------------
/feedbinclient/src/main/java/com/jocmp/feedbinclient/UpdateTagRequest.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.feedbinclient
2 |
3 | import com.squareup.moshi.JsonClass
4 |
5 | @JsonClass(generateAdapter = true)
6 | data class UpdateTagRequest(
7 | val old_name: String,
8 | val new_name: String
9 | )
10 |
--------------------------------------------------------------------------------
/feedbinclient/src/test/java/com/jocmp/feedbinclient/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jocmp/capyreader/699c32b80716b72a4fdf1dd650cf9f3e7e10f001/feedbinclient/src/test/java/com/jocmp/feedbinclient/.gitkeep
--------------------------------------------------------------------------------
/feedbinclient/src/test/java/com/jocmp/feedbinclient/PagingInfoTest.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.feedbinclient
2 |
3 | import org.junit.Assert.assertEquals
4 | import org.junit.Test
5 |
6 | class PagingInfoTest {
7 | @Test
8 | fun it_parses_a_header() {
9 | val linkHeader = "; rel=\"next\", ; rel=\"last\""
10 | val result = PagingInfo.fromHeader(linkHeader)
11 |
12 | assertEquals(PagingInfo(nextPage = 2, lastPage = 51), result)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/feedfinder/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/feedfinder/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("java-library")
3 | id("org.jetbrains.kotlin.jvm")
4 | id("com.google.devtools.ksp") version libs.versions.ksp
5 | }
6 |
7 | java {
8 | sourceCompatibility = JavaVersion.VERSION_21
9 | targetCompatibility = JavaVersion.VERSION_21
10 | }
11 |
12 | dependencies {
13 | implementation(libs.jsoup)
14 | implementation(libs.moshi)
15 | implementation(libs.kotlinx.coroutines.core)
16 | implementation(libs.okhttp.client)
17 | implementation(project(":rssparser"))
18 | ksp(libs.moshi.kotlin.codegen)
19 | testImplementation(kotlin("test"))
20 | testImplementation(libs.tests.junit)
21 | testImplementation(libs.tests.kotlinx.coroutines)
22 | }
23 |
--------------------------------------------------------------------------------
/feedfinder/src/main/java/com/jocmp/feedfinder/FeedError.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.feedfinder
2 |
3 | enum class FeedError {
4 | IO_FAILURE,
5 | INVALID_URL,
6 | NO_FEEDS_FOUND,
7 | }
8 |
9 | val FeedError.asException: FeedException
10 | get() = FeedException(this)
11 |
12 | class FeedException(val feedError: FeedError) : Exception(feedError.name)
13 |
--------------------------------------------------------------------------------
/feedfinder/src/main/java/com/jocmp/feedfinder/FeedFinder.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.feedfinder
2 |
3 | import com.jocmp.feedfinder.parser.Feed
4 | import com.jocmp.rssparser.model.RssChannel
5 |
6 | interface FeedFinder {
7 | suspend fun find(url: String): Result>
8 |
9 | suspend fun fetch(url: String): Result
10 | }
11 |
--------------------------------------------------------------------------------
/feedfinder/src/main/java/com/jocmp/feedfinder/OptionalURL.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.feedfinder
2 |
3 | import java.net.URI
4 | import java.net.URL
5 |
6 | internal fun optionalURL(string: String?): URL? {
7 | if (string.isNullOrBlank()) {
8 | return null
9 | }
10 |
11 | return try {
12 | URI(string).toURL()
13 | } catch (_: Throwable) {
14 | null
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/feedfinder/src/main/java/com/jocmp/feedfinder/Request.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.feedfinder
2 |
3 | import java.net.URL
4 |
5 | internal interface Request {
6 | suspend fun fetch(url: URL): Response
7 | }
8 |
--------------------------------------------------------------------------------
/feedfinder/src/main/java/com/jocmp/feedfinder/Response.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.feedfinder
2 |
3 | import com.jocmp.feedfinder.parser.Feed
4 | import com.jocmp.feedfinder.parser.Parser
5 | import java.net.URL
6 | import java.nio.charset.Charset
7 |
8 | internal class Response(
9 | val url: URL,
10 | val body: String,
11 | val charset: Charset?
12 | ) {
13 | suspend fun parse(validate: Boolean = true): Parser.Result {
14 | if (parsed == null) {
15 | parsed = Parser.parse(body, url = url, charset = charset, validate = validate)
16 | }
17 |
18 | return parsed!!
19 | }
20 |
21 | private var parsed: Parser.Result? = null
22 | }
23 |
24 | internal suspend fun Response.toParsedFeed(): Feed? {
25 | return (parse() as? Parser.Result.ParsedFeed)?.feed
26 | }
27 |
--------------------------------------------------------------------------------
/feedfinder/src/main/java/com/jocmp/feedfinder/StringProtocolExt.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.feedfinder
2 |
3 | val String.withProtocol: String
4 | get() {
5 | return if (!(startsWith("http") || startsWith("https"))) {
6 | "https://$this"
7 | } else {
8 | this
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/feedfinder/src/main/java/com/jocmp/feedfinder/parser/Feed.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.feedfinder.parser
2 |
3 | import com.jocmp.rssparser.model.RssItem
4 | import java.net.URL
5 |
6 | interface Feed {
7 | fun isValid(): Boolean
8 |
9 | val name: String
10 |
11 | val feedURL: URL
12 |
13 | val siteURL: URL?
14 |
15 | val faviconURL: URL?
16 |
17 | val items: List
18 | }
19 |
--------------------------------------------------------------------------------
/feedfinder/src/main/java/com/jocmp/feedfinder/sources/ResponseDocumentExt.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.feedfinder.sources
2 |
3 | import com.jocmp.feedfinder.Response
4 | import com.jocmp.feedfinder.parser.Parser
5 | import org.jsoup.nodes.Document
6 |
7 | internal suspend fun Response.findDocument(): Document? {
8 | val result = parse(validate = false)
9 |
10 | if (result is Parser.Result.HTMLDocument) {
11 | return result.document
12 | }
13 |
14 | return null
15 | }
16 |
--------------------------------------------------------------------------------
/feedfinder/src/main/java/com/jocmp/feedfinder/sources/Source.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.feedfinder.sources
2 |
3 | import com.jocmp.feedfinder.Request
4 | import com.jocmp.feedfinder.optionalURL
5 | import com.jocmp.feedfinder.parser.Feed
6 | import com.jocmp.feedfinder.parser.Parser
7 |
8 | internal sealed interface Source {
9 | suspend fun find(): List
10 |
11 | suspend fun createFromURL(url: String, fetcher: Request): Feed? {
12 | val parsedURL = optionalURL(url) ?: return null
13 |
14 | val response = fetcher.fetch(url = parsedURL)
15 |
16 | return when (val result = response.parse()) {
17 | is Parser.Result.ParsedFeed -> result.feed
18 | is Parser.Result.HTMLDocument -> null
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/feedfinder/src/main/java/com/jocmp/feedfinder/sources/XML.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.feedfinder.sources
2 |
3 | import com.jocmp.feedfinder.Response
4 | import com.jocmp.feedfinder.parser.Feed
5 | import com.jocmp.feedfinder.parser.Parser
6 | import com.jocmp.feedfinder.parser.Parser.Result.ParsedFeed
7 |
8 | internal class XML(private val response: Response): Source {
9 | override suspend fun find(): List {
10 | return try {
11 | val result = response.parse()
12 |
13 | if (result is ParsedFeed && result.feed.isValid()) {
14 | return listOf(result.feed)
15 | }
16 |
17 | emptyList()
18 | } catch (e: Parser.NotFeedError) {
19 | emptyList()
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/feedfinder/src/test/java/com/jocmp/feedfinder/Helpers.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.feedfinder
2 |
3 | import java.io.File
4 |
5 | fun testResource(resource: String): String {
6 | return "src/test/resources/${resource}"
7 | }
8 |
9 | fun testFile(resource: String): File {
10 | return File(testResource(resource))
11 | }
12 |
--------------------------------------------------------------------------------
/feedfinder/src/test/java/com/jocmp/feedfinder/TestRequest.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.feedfinder
2 |
3 | import java.io.File
4 | import java.net.URL
5 |
6 | internal class TestRequest(val sites: Map) : Request {
7 | override suspend fun fetch(url: URL): Response {
8 | val bodyPath = sites[url.toString()]
9 |
10 | val body = if (bodyPath == null) {
11 | ""
12 | } else {
13 | File(bodyPath).readText()
14 | }
15 |
16 | return Response(url = url, body = body, charset = null)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/feedfinder/src/test/java/com/jocmp/feedfinder/sources/XMLTest.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.feedfinder.sources
2 |
3 | import com.jocmp.feedfinder.Response
4 | import kotlinx.coroutines.runBlocking
5 | import org.junit.Test
6 | import java.io.File
7 | import java.net.URL
8 | import kotlin.test.assertEquals
9 |
10 | class XMLTest {
11 | @Test
12 | fun `it parses from an XML source`() = runBlocking {
13 | val body = File("src/test/resources/arstechnica_feed.xml").readText()
14 |
15 | val feeds = XML(Response(url = URL("https://arstechnica.com"), body = body, charset = null)).find()
16 |
17 | assertEquals(expected = 1, actual = feeds.size)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/feedfinder/src/test/resources/test_index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Title
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jocmp/capyreader/699c32b80716b72a4fdf1dd650cf9f3e7e10f001/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Nov 01 17:50:12 CDT 2024
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionSha256Sum=5b9c5eb3f9fc2c94abaea57d90bd78747ca117ddbbf96c859d3741181a12bf2a
5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/readerclient/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/readerclient/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("java-library")
3 | id("org.jetbrains.kotlin.jvm")
4 | id("com.google.devtools.ksp") version libs.versions.ksp
5 | }
6 |
7 | java {
8 | sourceCompatibility = JavaVersion.VERSION_21
9 | targetCompatibility = JavaVersion.VERSION_21
10 | }
11 |
12 | dependencies {
13 | implementation(libs.kotlinx.coroutines.core)
14 | implementation(libs.moshi)
15 | implementation(libs.moshi.converter)
16 | implementation(libs.retrofit2.retrofit)
17 | implementation(libs.converter.scalars)
18 | ksp(libs.moshi.kotlin.codegen)
19 | testImplementation(kotlin("test"))
20 | testImplementation(libs.tests.junit)
21 | testImplementation(libs.tests.kotlinx.coroutines)
22 | testImplementation(libs.tests.mockk.mockk)
23 | }
24 |
--------------------------------------------------------------------------------
/readerclient/src/main/java/com/jocmp/readerclient/Category.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.readerclient
2 |
3 | import com.squareup.moshi.JsonClass
4 |
5 | @JsonClass(generateAdapter = true)
6 | data class Category(
7 | val id: String,
8 | val label: String?,
9 | )
10 |
--------------------------------------------------------------------------------
/readerclient/src/main/java/com/jocmp/readerclient/ItemIdentifiers.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.readerclient
2 |
3 | object ItemIdentifiers {
4 | fun parseToHexID(numericID: String) = String.format("%016x", numericID.toLong())
5 | }
6 |
--------------------------------------------------------------------------------
/readerclient/src/main/java/com/jocmp/readerclient/ItemRef.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.readerclient
2 |
3 | import com.jocmp.readerclient.ItemIdentifiers.parseToHexID
4 | import com.squareup.moshi.JsonClass
5 |
6 | @JsonClass(generateAdapter = true)
7 | data class ItemRef(
8 | val id: String,
9 | ) {
10 | val hexID = parseToHexID(id)
11 | }
12 |
--------------------------------------------------------------------------------
/readerclient/src/main/java/com/jocmp/readerclient/Stream.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.readerclient
2 |
3 | sealed class Stream(val id: String) {
4 | class ReadingList: Stream("user/-/state/com.google/reading-list")
5 |
6 | class Starred: Stream("user/-/state/com.google/starred")
7 |
8 | class Read: Stream("user/-/state/com.google/read")
9 |
10 | class Feed(id: String): Stream(id)
11 |
12 | class Label(name: String): Stream("user/-/label/$name")
13 |
14 | class UserLabel(id: String): Stream(id)
15 | }
16 |
--------------------------------------------------------------------------------
/readerclient/src/main/java/com/jocmp/readerclient/StreamContentsResult.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.readerclient
2 |
3 | import com.squareup.moshi.JsonClass
4 |
5 | @JsonClass(generateAdapter = true)
6 | data class StreamContentsResult(
7 | val items: List- ,
8 | val continuation: String? = null,
9 | )
10 |
--------------------------------------------------------------------------------
/readerclient/src/main/java/com/jocmp/readerclient/StreamItemIDsResult.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.readerclient
2 |
3 | import com.squareup.moshi.JsonClass
4 |
5 | @JsonClass(generateAdapter = true)
6 | data class StreamItemIDsResult(
7 | val itemRefs: List,
8 | val continuation: String?,
9 | )
10 |
--------------------------------------------------------------------------------
/readerclient/src/main/java/com/jocmp/readerclient/StreamItemsContentsResult.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.readerclient
2 |
3 | import com.squareup.moshi.JsonClass
4 |
5 | @JsonClass(generateAdapter = true)
6 | data class StreamItemsContentsResult(
7 | val items: List
- ,
8 | )
9 |
--------------------------------------------------------------------------------
/readerclient/src/main/java/com/jocmp/readerclient/Subscription.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.readerclient
2 |
3 | import com.squareup.moshi.JsonClass
4 |
5 | @JsonClass(generateAdapter = true)
6 | data class Subscription(
7 | val id: String,
8 | val title: String,
9 | val categories: List,
10 | val url: String,
11 | val htmlUrl: String,
12 | val iconUrl: String,
13 | )
14 |
--------------------------------------------------------------------------------
/readerclient/src/main/java/com/jocmp/readerclient/SubscriptionEditAction.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.readerclient
2 |
3 |
4 | enum class SubscriptionEditAction(val id: String) {
5 | EDIT("edit"),
6 | UNSUBSCRIBE("unsubscribe")
7 | }
8 |
--------------------------------------------------------------------------------
/readerclient/src/main/java/com/jocmp/readerclient/SubscriptionListResult.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.readerclient
2 |
3 | import com.squareup.moshi.JsonClass
4 |
5 | @JsonClass(generateAdapter = true)
6 | data class SubscriptionListResult(
7 | val subscriptions: List
8 | )
9 |
--------------------------------------------------------------------------------
/readerclient/src/main/java/com/jocmp/readerclient/SubscriptionQuickAddResult.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.readerclient
2 |
3 | import com.squareup.moshi.JsonClass
4 |
5 | @JsonClass(generateAdapter = true)
6 | data class SubscriptionQuickAddResult(
7 | val numResults: Int?,
8 | val query: String? = null,
9 | val streamId: String? = null,
10 | val streamName: String? = null,
11 | )
12 |
--------------------------------------------------------------------------------
/readerclient/src/main/java/com/jocmp/readerclient/Tag.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.readerclient
2 |
3 | import com.squareup.moshi.Json
4 | import com.squareup.moshi.JsonClass
5 |
6 | @JsonClass(generateAdapter = true)
7 | data class Tag(
8 | val id: String,
9 | val type: Type?,
10 | ) {
11 | enum class Type {
12 | @Json(name = "folder")
13 | FOLDER,
14 | @Json(name = "tag")
15 | TAG
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/readerclient/src/main/java/com/jocmp/readerclient/TagListResult.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.readerclient
2 |
3 | import com.squareup.moshi.JsonClass
4 |
5 | @JsonClass(generateAdapter = true)
6 | data class TagListResult(
7 | val tags: List
8 | )
9 |
--------------------------------------------------------------------------------
/rssparser/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/rssparser/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("java-library")
3 | id("com.google.devtools.ksp") version libs.versions.ksp
4 | alias(libs.plugins.jetbrains.kotlin.jvm)
5 | }
6 |
7 | java {
8 | sourceCompatibility = JavaVersion.VERSION_21
9 | targetCompatibility = JavaVersion.VERSION_21
10 | }
11 |
12 | dependencies {
13 | implementation(libs.okhttp.client)
14 | implementation(libs.kotlinx.coroutines.core)
15 | implementation(libs.jsoup)
16 | implementation(libs.moshi)
17 | implementation(libs.moshi.converter)
18 | ksp(libs.moshi.kotlin.codegen)
19 | testImplementation(kotlin("test"))
20 | testImplementation(kotlin("test-common"))
21 | testImplementation(kotlin("test-annotations-common"))
22 | testImplementation(libs.tests.kotlinx.coroutines)
23 | }
24 |
--------------------------------------------------------------------------------
/rssparser/src/main/kotlin/com/jocmp/rssparser/exception/HttpException.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.rssparser.exception
2 |
3 | /**
4 | * An exception thrown when an HTTP call fails with an HTTP error
5 | *
6 | * @property code the HTTP error code
7 | * @property message the detail message string.
8 | */
9 | data class HttpException(
10 | val code: Int,
11 | override val message: String?,
12 | ) : Exception()
13 |
--------------------------------------------------------------------------------
/rssparser/src/main/kotlin/com/jocmp/rssparser/exception/RssParsingException.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.rssparser.exception
2 |
3 | /**
4 | * An exception thrown whe the parsing of the RSS feed fails
5 | *
6 | * @property message the detail message string.
7 | * @property cause the cause of this throwable.
8 | */
9 | data class RssParsingException(
10 | override val message: String?,
11 | override val cause: Throwable?,
12 | ) : Exception()
13 |
--------------------------------------------------------------------------------
/rssparser/src/main/kotlin/com/jocmp/rssparser/internal/FeedHandler.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.rssparser.internal
2 |
3 | import com.jocmp.rssparser.model.RssChannel
4 |
5 | internal interface FeedHandler {
6 | fun build(): RssChannel
7 | }
8 |
--------------------------------------------------------------------------------
/rssparser/src/main/kotlin/com/jocmp/rssparser/internal/Fetcher.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.rssparser.internal
2 |
3 | internal interface Fetcher {
4 | suspend fun fetch(url: String): ParserInput
5 | }
6 |
--------------------------------------------------------------------------------
/rssparser/src/main/kotlin/com/jocmp/rssparser/internal/ImagePolicy.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.rssparser.internal
2 |
3 | internal object ImagePolicy {
4 | fun isValidArticleImage(src: String): Boolean {
5 | return src.isNotBlank() &&
6 | !src.contains(EMOJI_DOMAIN) &&
7 | !src.contains(GRAVATAR_DOMAIN)
8 | }
9 |
10 | private const val EMOJI_DOMAIN = "s.w.org/images/core/emoji"
11 | private const val GRAVATAR_DOMAIN = "gravatar.com"
12 | }
13 |
--------------------------------------------------------------------------------
/rssparser/src/main/kotlin/com/jocmp/rssparser/internal/Parser.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.rssparser.internal
2 |
3 | import com.jocmp.rssparser.model.RssChannel
4 |
5 | internal interface Parser {
6 | suspend fun parse(input: ParserInput): RssChannel
7 | }
8 |
--------------------------------------------------------------------------------
/rssparser/src/main/kotlin/com/jocmp/rssparser/internal/ParserInput.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.rssparser.internal
2 |
3 | import java.io.InputStream
4 | import java.nio.charset.Charset
5 |
6 | internal class ParserInput(private val bytes: ByteArray, val charset: Charset?) {
7 | fun inputStream() = bytes.inputStream()
8 |
9 | companion object {
10 | fun from(inputStream: InputStream, charset: Charset?) =
11 | ParserInput(inputStream.readBytes(), charset = charset)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/rssparser/src/main/kotlin/com/jocmp/rssparser/internal/json/models/Author.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.rssparser.internal.json.models
2 |
3 | import com.squareup.moshi.JsonClass
4 |
5 | @JsonClass(generateAdapter = true)
6 | internal data class Author(
7 | val name: String?,
8 | val url: String?,
9 | val avatar: String?,
10 | )
11 |
--------------------------------------------------------------------------------
/rssparser/src/main/kotlin/com/jocmp/rssparser/internal/json/models/Feed.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.rssparser.internal.json.models
2 |
3 | import com.squareup.moshi.JsonClass
4 |
5 | @JsonClass(generateAdapter = true)
6 | internal data class Feed(
7 | val version: String,
8 | val title: String,
9 | val home_page_url: String?,
10 | val feed_url: String?,
11 | val description: String?,
12 | val user_comment: String?,
13 | val next_url: String?,
14 | val icon: String?,
15 | val favicon: String?,
16 | val authors: List?,
17 | val language: String?,
18 | val expired: Boolean?,
19 | val hubs: List?,
20 | val items: List
- ,
21 | )
22 |
--------------------------------------------------------------------------------
/rssparser/src/main/kotlin/com/jocmp/rssparser/internal/json/models/Hub.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.rssparser.internal.json.models
2 |
3 | import com.squareup.moshi.JsonClass
4 |
5 | @JsonClass(generateAdapter = true)
6 | internal data class Hub(
7 | val type: String,
8 | val url: String,
9 | )
10 |
--------------------------------------------------------------------------------
/rssparser/src/main/kotlin/com/jocmp/rssparser/internal/json/models/Item.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.rssparser.internal.json.models
2 |
3 | import com.squareup.moshi.JsonClass
4 |
5 | @JsonClass(generateAdapter = true)
6 | internal data class Item(
7 | val id: String,
8 | val url: String?,
9 | val external_url: String?,
10 | val title: String?,
11 | val content_html: String?,
12 | val content_text: String?,
13 | val summary: String?,
14 | val image: String?,
15 | val banner_image: String?,
16 | val date_published: String?,
17 | val date_modified: String?,
18 | val authors: List?,
19 | val tags: List?,
20 | val language: String?,
21 | )
22 |
--------------------------------------------------------------------------------
/rssparser/src/main/kotlin/com/jocmp/rssparser/model/RssItemEnclosure.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.rssparser.model
2 |
3 | data class RssItemEnclosure(
4 | val url: String,
5 | val type: String,
6 | )
7 |
--------------------------------------------------------------------------------
/rssparser/src/test/kotlin/com/jocmp/rssparser/ParserFactory.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.rssparser
2 |
3 | import com.jocmp.rssparser.internal.DefaultParser
4 | import com.jocmp.rssparser.internal.Parser
5 | import kotlinx.coroutines.ExperimentalCoroutinesApi
6 | import kotlinx.coroutines.test.UnconfinedTestDispatcher
7 |
8 | internal object ParserFactory {
9 | @OptIn(ExperimentalCoroutinesApi::class)
10 | fun build(): Parser = DefaultParser(dispatcher = UnconfinedTestDispatcher())
11 | }
12 |
--------------------------------------------------------------------------------
/rssparser/src/test/kotlin/com/jocmp/rssparser/TestUtils.kt:
--------------------------------------------------------------------------------
1 | package com.jocmp.rssparser
2 |
3 | import com.jocmp.rssparser.internal.ParserInput
4 | import java.io.File
5 | import java.nio.charset.Charset
6 |
7 | internal fun readFileFromResources(
8 | resourceName: String,
9 | charset: Charset? = null,
10 | ): ParserInput {
11 | val file = File("src/test/resources/$resourceName")
12 |
13 | return ParserInput(file.readBytes(), charset = charset)
14 | }
15 |
--------------------------------------------------------------------------------
/rssparser/src/test/resources/feed-test-accents.xml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jocmp/capyreader/699c32b80716b72a4fdf1dd650cf9f3e7e10f001/rssparser/src/test/resources/feed-test-accents.xml
--------------------------------------------------------------------------------
/rssparser/src/test/resources/feed-test-greek.xml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jocmp/capyreader/699c32b80716b72a4fdf1dd650cf9f3e7e10f001/rssparser/src/test/resources/feed-test-greek.xml
--------------------------------------------------------------------------------
/scripts/base64_encode:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | # frozen_string_literal: true
4 |
5 | require 'base64'
6 |
7 | file_body = File.open(ARGV[0]).read
8 |
9 | puts Base64.strict_encode64(file_body)
10 |
--------------------------------------------------------------------------------
/scripts/changelog:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require 'open3'
3 | require 'fileutils'
4 |
5 | def fetch_config
6 | result, _, _ = Open3.capture3("bumpver show -n --environ")
7 |
8 | result
9 | .split(' ')
10 | .map { |pair| pair.split('=', 2) }
11 | .to_h
12 | end
13 |
14 | build = fetch_config["BID"].to_i
15 | next_build = build + 1
16 |
17 | path = "fastlane/metadata/android/en-US/changelogs/#{next_build}.txt"
18 | FileUtils.touch "./#{path}"
19 |
20 | puts "Added changelog for next build (#{next_build})\n./#{path}"
21 |
22 | exec("code", path)
23 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | mavenCentral()
5 | google()
6 | maven(url = "https://oss.sonatype.org/content/repositories/snapshots")
7 | }
8 | }
9 |
10 | dependencyResolutionManagement {
11 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
12 | repositories {
13 | google()
14 | mavenCentral()
15 | maven { setUrl("https://jitpack.io") }
16 | maven(url = "https://oss.sonatype.org/content/repositories/snapshots")
17 | }
18 | }
19 |
20 | rootProject.name = "Capy Reader"
21 | include(":app")
22 | include(":feedbinclient")
23 | include(":feedfinder")
24 | include(":capy")
25 | include(":rssparser")
26 | include(":readerclient")
27 |
--------------------------------------------------------------------------------
/site/capy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jocmp/capyreader/699c32b80716b72a4fdf1dd650cf9f3e7e10f001/site/capy.png
--------------------------------------------------------------------------------
/site/play-badge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jocmp/capyreader/699c32b80716b72a4fdf1dd650cf9f3e7e10f001/site/play-badge.png
--------------------------------------------------------------------------------
/technotes/AuthFlow.md:
--------------------------------------------------------------------------------
1 | # Auth Flow
2 |
3 | On start up
4 |
5 | 1. If the user has not authenticated, prompt with auth modal
6 | 2. If the user is authenticated, display list
7 |
8 | On log out, do nothing. Will reprompt via startup
9 |
10 |
11 | - https://developer.android.com/reference/androidx/security/crypto/EncryptedSharedPreferences
12 |
--------------------------------------------------------------------------------
/technotes/FeedbinAPI.md:
--------------------------------------------------------------------------------
1 | # Subscription
2 |
3 |
4 |
5 | 1 user has many subscriptions
6 | 1 Subscription belongs to 1 Feed
7 |
8 | # Feeds
9 |
10 |
11 |
12 | A feed is a global entity.
13 |
14 | 1 feed has many subscriptions
15 |
16 | # Entries
17 |
18 |
19 |
20 | A user has many entries through their subscriptions' feed.
--------------------------------------------------------------------------------
/technotes/Fonts.md:
--------------------------------------------------------------------------------
1 |
2 | Google Provides downloadable fonts as a part of the AndroidX library. However, this implementation is dependent on GMS to sign each download.
3 |
4 |
5 | Questions
6 |
7 | - How much space does a font take up?
8 | - 1 or 2MB more for fonts is an acceptable trade-off to avoid the GMS dependency
9 | - Can a font be shared between `/res` and the webview?
10 |
11 | Reference
12 | - https://developer.android.com/develop/ui/compose/text/fonts#variable-fonts
13 | - https://developer.android.com/jetpack/compose/text/fonts#downloadable-fonts
14 | - https://stackoverflow.com/a/4338273
15 | - https://developer.android.com/develop/ui/views/text-and-emoji/fonts-in-xml
16 |
--------------------------------------------------------------------------------
/technotes/Notifications.md:
--------------------------------------------------------------------------------
1 | ```kotlin
2 | if (BuildConfig.DEBUG) {
3 | val context = LocalContext.current
4 | Button(onClick = {
5 | val request = OneTimeWorkRequestBuilder()
6 | .build()
7 |
8 | WorkManager.getInstance(context).enqueue(request)
9 | }) {
10 | Text("BG WORK")
11 | }
12 | }
13 | ```
--------------------------------------------------------------------------------
/technotes/Parsing.md:
--------------------------------------------------------------------------------
1 | # Feed Finder
2 |
3 | 1. XMLFeed
4 | 2. JSONFeed (wontdo)
5 | 3. HTML
6 | a. Meta Links
7 | b. Body Links
8 | 4. XMLFeedGuess (second pass)
9 |
10 | - XML can be parsed directly if XML feed
11 | - HTML takes response body
12 |
13 | ## Reading from File
14 |
15 | 1. Parse from OPML
16 | 2. Find all feeds based on IDs
17 | 3. Map feed from DB to set
18 |
19 | ## Timestamps
20 |
21 | The `pubDate` field may not be a ISO 8601 format instead using the following format
22 |
23 | ```
24 | D, d M Y H:i:s O
25 | ```
26 |
27 | Example:
28 |
29 | - Sat, 29 Jun 2024 10:55:08 +0000
30 |
31 |
--------------------------------------------------------------------------------
/technotes/Scratch/2024-11 Mercury Parser.md:
--------------------------------------------------------------------------------
1 | ```kotlin
2 | webView.evaluateJavascript(
3 | """(function test() {
4 | return "hello";
5 | })();
6 | """.trimIndent()) {
7 | it
8 | }
9 | ```
10 |
11 | 1. Load placeholder text
12 | 2. Fetch full content
13 | 3. evaluateJavascript -> Parser(content)
--------------------------------------------------------------------------------