├── .github ├── FUNDING.yml ├── ci-gradle.properties └── workflows │ ├── build_apk.yml │ └── check.yml ├── .gitignore ├── LICENSE ├── README.md ├── app-hosting ├── build.gradle.kts └── src │ ├── androidMain │ └── kotlin │ │ └── com │ │ └── zhangke │ │ └── fread │ │ ├── HostingApplication.kt │ │ ├── composable │ │ └── LoadingPage.android.kt │ │ ├── di │ │ ├── AndroidActivityComponent.kt │ │ └── AndroidApplicationComponent.kt │ │ ├── screen │ │ └── AndroidFreadApp.kt │ │ └── utils │ │ └── ActivityHelper.android.kt │ ├── commonMain │ ├── composeResources │ │ ├── values-zh │ │ │ └── strings.xml │ │ └── values │ │ │ └── strings.xml │ └── kotlin │ │ └── com │ │ └── zhangke │ │ └── fread │ │ ├── auth │ │ └── AuthenticationPage.kt │ │ ├── composable │ │ └── LoadingPage.kt │ │ ├── di │ │ ├── HostingActivityComponent.kt │ │ └── HostingApplicationComponent.kt │ │ ├── screen │ │ ├── FreadApp.kt │ │ ├── FreadScreen.kt │ │ └── main │ │ │ ├── MainPage.kt │ │ │ ├── MainPageUiState.kt │ │ │ ├── MainViewModel.kt │ │ │ └── drawer │ │ │ ├── MainDrawer.kt │ │ │ ├── MainDrawerUiState.kt │ │ │ └── MainDrawerViewModel.kt │ │ └── utils │ │ └── ActivityHelper.kt │ └── iosMain │ └── kotlin │ └── com │ └── zhangke │ └── fread │ ├── di │ ├── IosActivityComponent.kt │ └── IosApplicationComponent.kt │ ├── screen │ ├── FreadViewController.kt │ └── IosFreadApp.kt │ ├── startup │ └── KRouterStartup.kt │ └── utils │ └── ActivityHelper.ios.kt ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── debug │ └── java │ │ └── com │ │ └── zhangke │ │ └── fread │ │ └── debug │ │ └── screens │ │ ├── collapsable │ │ └── CollapsableDemoScreen.kt │ │ ├── coroutine │ │ └── CoroutineDemoScreen.kt │ │ ├── media │ │ ├── DoubleImageTestScreen.kt │ │ ├── FiveImageMediaTestScreen.kt │ │ ├── ImageMediaTestScreen.kt │ │ ├── QuadrupleImageMediaTestScreen.kt │ │ ├── SingleImageTestScreen.kt │ │ ├── SixfoldImageTestScreen.kt │ │ └── ThreeImageMediaTestScreen.kt │ │ ├── poll │ │ └── BlogPollTestScreen.kt │ │ ├── scroll │ │ └── ScrollDemoScreen.kt │ │ └── video │ │ ├── FullVideoPlayDemoScreen.kt │ │ └── InlineVideoPlayerScreen.kt │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ └── com │ │ └── zhangke │ │ └── fread │ │ ├── FreadApplication.kt │ │ └── screen │ │ └── FreadActivity.kt │ └── res │ ├── drawable │ ├── ic_launcher_foreground.xml │ └── shape_alert_dialog_background.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-mdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── values-night │ └── themes.xml │ ├── values-zh │ └── strings.xml │ └── values │ ├── colors.xml │ ├── ic_launcher_background.xml │ ├── strings.xml │ └── themes.xml ├── appprivacy.html ├── bizframework └── status-provider │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ ├── androidMain │ └── kotlin │ │ └── com │ │ └── zhangke │ │ └── fread │ │ └── status │ │ ├── richtext │ │ └── RichTextBuilder.android.kt │ │ └── utils │ │ └── ImplementerFinder.kt │ ├── commonMain │ ├── composeResources │ │ ├── values-zh │ │ │ └── strings.xml │ │ └── values │ │ │ └── strings.xml │ └── kotlin │ │ └── com │ │ └── zhangke │ │ └── fread │ │ └── status │ │ ├── StatusProvider.kt │ │ ├── account │ │ ├── AccountManager.kt │ │ ├── AuthenticationFailureException.kt │ │ └── LoggedAccount.kt │ │ ├── author │ │ └── BlogAuthor.kt │ │ ├── blog │ │ ├── Blog.kt │ │ ├── BlogEmbed.kt │ │ ├── BlogMedia.kt │ │ ├── BlogMediaMeta.kt │ │ ├── BlogMediaType.kt │ │ ├── BlogPoll.kt │ │ ├── BlogServer.kt │ │ ├── BlogTranslation.kt │ │ └── PostingApplication.kt │ │ ├── content │ │ ├── ContentManager.kt │ │ └── MixedContent.kt │ │ ├── model │ │ ├── ContentConfig.kt │ │ ├── ContentType.kt │ │ ├── Emoji.kt │ │ ├── FacetFeatureUnion.kt │ │ ├── FormattingTime.kt │ │ ├── FreadContent.kt │ │ ├── Hashtag.kt │ │ ├── HashtagInStatus.kt │ │ ├── IdentityRole.kt │ │ ├── Mention.kt │ │ ├── PagedData.kt │ │ ├── PostInteractionSetting.kt │ │ ├── PublishBlogRules.kt │ │ ├── StatusActionType.kt │ │ ├── StatusList.kt │ │ ├── StatusProviderProtocol.kt │ │ ├── StatusUiState.kt │ │ └── StatusVisibility.kt │ │ ├── notification │ │ ├── NotificationResolver.kt │ │ └── StatusNotification.kt │ │ ├── platform │ │ ├── BlogPlatform.kt │ │ ├── PlatformResolver.kt │ │ └── PlatformSnapshot.kt │ │ ├── publish │ │ ├── PublishBlogManager.kt │ │ └── PublishingPost.kt │ │ ├── richtext │ │ ├── OnLinkTargetClick.kt │ │ ├── RichText.kt │ │ ├── RichTextBuilder.kt │ │ ├── model │ │ │ └── RichLinkTarget.kt │ │ └── parser │ │ │ ├── FacetHtmlParser.kt │ │ │ └── HtmlParser.kt │ │ ├── screen │ │ └── StatusScreenProvider.kt │ │ ├── search │ │ ├── SearchContentResult.kt │ │ ├── SearchEngine.kt │ │ └── SearchResult.kt │ │ ├── source │ │ ├── StatusSource.kt │ │ └── StatusSourceResolver.kt │ │ ├── status │ │ ├── StatusResolver.kt │ │ └── model │ │ │ ├── Status.kt │ │ │ └── StatusContext.kt │ │ ├── uri │ │ ├── FormalUri.kt │ │ └── FormalUriParser.kt │ │ └── utils │ │ ├── DateTimeFormatter.kt │ │ └── ResultKtx.kt │ └── commonTest │ └── kotlin │ └── com │ └── zhangke │ └── fread │ └── status │ ├── model │ └── IdentityRoleTest.kt │ ├── richtext │ └── parser │ │ └── HtmlParserTest.kt │ └── uri │ └── FormalUriTest.kt ├── build-logic ├── README.md ├── convention │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── kotlin │ │ ├── AndroidApplicationConventionPlugin.kt │ │ ├── AndroidLibraryConventionPlugin.kt │ │ ├── ComposeMultiPlatformConventionPlugin.kt │ │ ├── KotlinMultiplatformLibraryConventionPlugin.kt │ │ ├── Project.kt │ │ ├── ProjectFeatureKmpConventionPlugin.kt │ │ ├── ProjectFrameworkKmpConventionPlugin.kt │ │ └── com │ │ └── zhangke │ │ └── fread │ │ ├── KotlinAndroid.kt │ │ ├── PrintTestApks.kt │ │ └── ProjectExt.kt ├── gradle.properties └── settings.gradle.kts ├── build.gradle.kts ├── commonbiz ├── analytics │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ │ ├── androidMain │ │ └── kotlin │ │ │ └── com │ │ │ └── zhangke │ │ │ └── fread │ │ │ └── analytics │ │ │ └── TrackingScreenEvent.android.kt │ │ ├── commonMain │ │ └── kotlin │ │ │ └── com │ │ │ └── zhangke │ │ │ └── fread │ │ │ └── analytics │ │ │ ├── Analytics.kt │ │ │ ├── AnalyticsScreenHook.kt │ │ │ ├── AnalyticsTabHook.kt │ │ │ ├── EventNames.kt │ │ │ ├── TrackingEventDataBuilder.kt │ │ │ └── TrackingScreenEvent.kt │ │ └── iosMain │ │ └── kotlin │ │ └── com │ │ └── zhangke │ │ └── fread │ │ └── analytics │ │ └── TrackingScreenEvent.ios.kt ├── common │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ │ ├── androidMain │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── zhangke │ │ │ │ └── fread │ │ │ │ └── common │ │ │ │ ├── CommonActivityComponent.android.kt │ │ │ │ ├── CommonComponent.android.kt │ │ │ │ ├── browser │ │ │ │ ├── BrowserBridgeDialogActivity.kt │ │ │ │ ├── BrowserBridgeDialogActivityComponent.kt │ │ │ │ ├── BrowserLauncher.android.kt │ │ │ │ └── OAuthLauncher.android.kt │ │ │ │ ├── daynight │ │ │ │ └── DayNightHelper.android.kt │ │ │ │ ├── di │ │ │ │ └── ApplicationContext.kt │ │ │ │ ├── ext │ │ │ │ └── InstantExt.android.kt │ │ │ │ ├── handler │ │ │ │ └── TextHandler.android.kt │ │ │ │ ├── language │ │ │ │ └── LanguageHelper.android.kt │ │ │ │ ├── page │ │ │ │ └── BasePagerTabHookManager.android.kt │ │ │ │ ├── startup │ │ │ │ └── LanguageModuleStartup.kt │ │ │ │ ├── update │ │ │ │ └── AppPlatformUpdater.android.kt │ │ │ │ └── utils │ │ │ │ ├── ActivityResultUtils.kt │ │ │ │ ├── DateTypeConverter.kt │ │ │ │ ├── HashtagTextUtils.kt │ │ │ │ ├── MediaFileHelper.android.kt │ │ │ │ ├── PlatformUriHelper.android.kt │ │ │ │ ├── RandomIdGenerator.android.kt │ │ │ │ ├── ShareHelper.kt │ │ │ │ ├── StorageHelper.android.kt │ │ │ │ ├── ThumbnailHelper.android.kt │ │ │ │ └── ToastHelper.android.kt │ │ └── res │ │ │ ├── anim │ │ │ ├── fade_in.xml │ │ │ └── fade_out.xml │ │ │ ├── drawable-hdpi │ │ │ └── ic_fread_logo_small_circle.png │ │ │ ├── drawable-mdpi │ │ │ └── ic_fread_logo_small_circle.png │ │ │ ├── drawable-xhdpi │ │ │ └── ic_fread_logo_small_circle.png │ │ │ ├── drawable-xxhdpi │ │ │ └── ic_fread_logo_small_circle.png │ │ │ ├── drawable-xxxhdpi │ │ │ ├── ic_fread_logo.webp │ │ │ └── ic_fread_logo_small_circle.png │ │ │ └── values │ │ │ ├── colors.xml │ │ │ └── theme.xml │ │ ├── commonMain │ │ ├── composeResources │ │ │ ├── drawable-xxxhdpi │ │ │ │ ├── ic_fread_logo.webp │ │ │ │ ├── illustration_cards.png │ │ │ │ ├── illustration_celebrate.png │ │ │ │ ├── illustration_explorer.png │ │ │ │ ├── illustration_inspiration.png │ │ │ │ └── illustration_message.png │ │ │ ├── drawable │ │ │ │ ├── bluesky_logo.xml │ │ │ │ ├── ic_logo_small.xml │ │ │ │ └── mastodon_logo.xml │ │ │ ├── values-zh │ │ │ │ └── strings.xml │ │ │ └── values │ │ │ │ └── strings.xml │ │ └── kotlin │ │ │ ├── cafe │ │ │ └── adriel │ │ │ │ └── voyager │ │ │ │ └── hilt │ │ │ │ └── ViewModel.kt │ │ │ └── com │ │ │ └── zhangke │ │ │ └── fread │ │ │ └── common │ │ │ ├── CommonActivityComponent.kt │ │ │ ├── CommonComponent.kt │ │ │ ├── MixedContentJsonBuilder.kt │ │ │ ├── NavigatorExit.kt │ │ │ ├── action │ │ │ ├── ComposableActions.kt │ │ │ ├── RouteAction.kt │ │ │ └── RouteActions.kt │ │ │ ├── adapter │ │ │ └── StatusUiStateAdapter.kt │ │ │ ├── browser │ │ │ ├── BrowserInterceptor.kt │ │ │ ├── BrowserLauncher.kt │ │ │ └── OAuthHandler.kt │ │ │ ├── bubble │ │ │ ├── Bubble.kt │ │ │ └── BubbleManager.kt │ │ │ ├── config │ │ │ ├── AppCommonConfig.kt │ │ │ ├── FreadConfigManager.kt │ │ │ ├── LocalConfigManager.kt │ │ │ ├── StatusConfig.kt │ │ │ └── StatusContentSize.kt │ │ │ ├── content │ │ │ └── FreadContentRepo.kt │ │ │ ├── daynight │ │ │ └── DayNightHelper.kt │ │ │ ├── db │ │ │ ├── ContentConfigDatabases.kt │ │ │ ├── FreadContentDatabase.kt │ │ │ ├── MixedStatusDatabases.kt │ │ │ └── converts │ │ │ │ ├── BlogMediaConverterHelper.kt │ │ │ │ ├── BlogMediaListConverter.kt │ │ │ │ ├── BlogPollConverter.kt │ │ │ │ ├── ContentTabConverter.kt │ │ │ │ ├── ContentTypeConverter.kt │ │ │ │ ├── FormalBaseUrlConverter.kt │ │ │ │ ├── FormalUriConverter.kt │ │ │ │ ├── FreadContentConverter.kt │ │ │ │ ├── IdentityRoleConverter.kt │ │ │ │ ├── StatusConverter.kt │ │ │ │ ├── StatusNotificationConverter.kt │ │ │ │ ├── StatusProviderUriListConverter.kt │ │ │ │ └── StatusUiStateConverter.kt │ │ │ ├── di │ │ │ ├── ApplicationCoroutineScope.kt │ │ │ ├── Scopes.kt │ │ │ └── ViewModelFactory.kt │ │ │ ├── feeds │ │ │ └── model │ │ │ │ └── RefreshResult.kt │ │ │ ├── handler │ │ │ └── ActivityTextHandler.kt │ │ │ ├── language │ │ │ └── LanguageHelper.kt │ │ │ ├── mixed │ │ │ └── MixedStatusRepo.kt │ │ │ ├── page │ │ │ ├── BasePagerTab.kt │ │ │ ├── BasePagerTabHookManager.kt │ │ │ ├── BaseScreen.kt │ │ │ └── BaseScreenHookManager.kt │ │ │ ├── publish │ │ │ └── PublishPostManager.kt │ │ │ ├── push │ │ │ └── IPushManager.kt │ │ │ ├── resources │ │ │ └── StatusProviderLogos.kt │ │ │ ├── review │ │ │ ├── DefaultAppStoreReviewer.kt │ │ │ └── FreadReviewManager.kt │ │ │ ├── startup │ │ │ ├── FeedsRepoModuleStartup.kt │ │ │ ├── FreadConfigModuleStartup.kt │ │ │ └── StartupManager.kt │ │ │ ├── status │ │ │ ├── StatusConfiguration.kt │ │ │ ├── StatusIdGenerator.kt │ │ │ ├── StatusUpdater.kt │ │ │ ├── adapter │ │ │ │ └── ContentConfigAdapter.kt │ │ │ ├── model │ │ │ │ └── SearchResultUiState.kt │ │ │ └── usecase │ │ │ │ └── FormatStatusDisplayTimeUseCase.kt │ │ │ ├── update │ │ │ ├── AppPlatformUpdater.kt │ │ │ ├── AppReleaseInfo.kt │ │ │ └── AppUpdateManager.kt │ │ │ ├── usecase │ │ │ └── GetDefaultBaseUrlUseCase.kt │ │ │ └── utils │ │ │ ├── GlobalScreenNavigation.kt │ │ │ ├── InstantExt.kt │ │ │ ├── ListStringConverter.kt │ │ │ ├── MediaFileHelper.kt │ │ │ ├── MentionTextUtil.kt │ │ │ ├── PlatformUriHelper.kt │ │ │ ├── RandomIdGenerator.kt │ │ │ ├── StorageHelper.kt │ │ │ ├── ThumbnailHelper.kt │ │ │ ├── ToastHelper.kt │ │ │ └── WebFingerConverter.kt │ │ ├── commonTest │ │ └── kotlin │ │ │ └── com │ │ │ └── zhangke │ │ │ └── fread │ │ │ └── common │ │ │ ├── status │ │ │ └── utils │ │ │ │ └── createStatus.kt │ │ │ └── utils │ │ │ ├── DateTimeFormatterTest.kt │ │ │ └── FormalUriTest.kt │ │ └── iosMain │ │ └── kotlin │ │ └── com │ │ └── zhangke │ │ └── fread │ │ └── common │ │ ├── CommonActivityComponent.ios.kt │ │ ├── CommonComponent.ios.kt │ │ ├── browser │ │ ├── BrowserLauncher.ios.kt │ │ └── OAuthLauncher.ios.kt │ │ ├── daynight │ │ └── DayNightHelper.ios.kt │ │ ├── handler │ │ └── TextHandler.ios.kt │ │ ├── language │ │ └── LanguageHelper.ios.kt │ │ ├── page │ │ └── BasePagerTabHookManager.ios.kt │ │ ├── update │ │ └── AppPlatformUpdater.ios.kt │ │ └── utils │ │ ├── MediaFileHelper.ios.kt │ │ ├── PlatformUriHelper.ios.kt │ │ ├── RandomIdGenerator.ios.kt │ │ ├── StorageHelper.ios.kt │ │ ├── SystemUtils.kt │ │ ├── ThumbnailHelper.ios.kt │ │ └── ToastHelper.ios.kt ├── sharedscreen │ ├── .gitignore │ ├── build.gradle.kts │ ├── proguard-rules.pro │ └── src │ │ ├── androidMain │ │ └── kotlin │ │ │ └── com │ │ │ └── zhangke │ │ │ └── fread │ │ │ └── commonbiz │ │ │ └── shared │ │ │ ├── SharedScreenPlatformModule.android.kt │ │ │ ├── composable │ │ │ ├── OnBlogMediaClick.android.kt │ │ │ └── WebViewPreviewer.android.kt │ │ │ └── screen │ │ │ ├── FullVideoScreen.kt │ │ │ └── ImageViewerScreen.android.kt │ │ ├── commonMain │ │ ├── composeResources │ │ │ ├── values-zh │ │ │ │ └── strings.xml │ │ │ └── values │ │ │ │ └── strings.xml │ │ └── kotlin │ │ │ └── com │ │ │ └── zhangke │ │ │ └── fread │ │ │ └── commonbiz │ │ │ └── shared │ │ │ ├── ModuleScreenVisitor.kt │ │ │ ├── SharedScreenModelModule.kt │ │ │ ├── blog │ │ │ └── detail │ │ │ │ ├── BlogDetailViewModel.kt │ │ │ │ └── RssBlogDetailScreen.kt │ │ │ ├── composable │ │ │ ├── BlogUiInNotification.kt │ │ │ ├── FeedsContent.kt │ │ │ ├── FeedsStatusNode.kt │ │ │ ├── ObserveForFeedsConnection.kt │ │ │ ├── OnBlogMediaClick.kt │ │ │ ├── SearchResultUi.kt │ │ │ └── WebViewPreviewer.kt │ │ │ ├── db │ │ │ └── SelectedAccountPublishingDatabase.kt │ │ │ ├── feeds │ │ │ ├── CommonFeedsUiState.kt │ │ │ ├── FeedsViewModelController.kt │ │ │ ├── IFeedsViewModelController.kt │ │ │ ├── IInteractiveHandler.kt │ │ │ ├── InteractiveHandleResult.kt │ │ │ └── InteractiveHandler.kt │ │ │ ├── notification │ │ │ ├── BlogInteractionNotification.kt │ │ │ ├── FavouriteNotification.kt │ │ │ ├── FollowNotification.kt │ │ │ ├── FollowRequestNotification.kt │ │ │ ├── NotificationHeadLine.kt │ │ │ ├── NotificationWithWholeStatus.kt │ │ │ ├── PollNotification.kt │ │ │ ├── SeveredRelationshipsNotification.kt │ │ │ ├── StatusNotificationUi.kt │ │ │ └── UnknownNotification.kt │ │ │ ├── repo │ │ │ └── SelectedAccountPublishingRepo.kt │ │ │ ├── screen │ │ │ ├── ImageViewerScreen.kt │ │ │ ├── SelectLanguageScreen.kt │ │ │ ├── publish │ │ │ │ ├── PublishBlogScreen.kt │ │ │ │ ├── PublishBlogUiState.kt │ │ │ │ ├── PublishPostBottomPanel.kt │ │ │ │ ├── PublishPostMediaAttachment.kt │ │ │ │ ├── PublishPostScaffold.kt │ │ │ │ ├── PublishSettingLabel.kt │ │ │ │ ├── PublishTopBar.kt │ │ │ │ ├── composable │ │ │ │ │ ├── AvatarsHorizontalStack.kt │ │ │ │ │ ├── BlogMediaAttachment.kt │ │ │ │ │ ├── InputBlogTextField.kt │ │ │ │ │ ├── PostInteractionSettingLabel.kt │ │ │ │ │ ├── PostStatusVisibilityUi.kt │ │ │ │ │ └── PostStatusWarning.kt │ │ │ │ ├── model │ │ │ │ │ └── PublishBlogMediaAttachment.kt │ │ │ │ └── multi │ │ │ │ │ ├── MultiAccountPublishingScreen.kt │ │ │ │ │ ├── MultiAccountPublishingUiState.kt │ │ │ │ │ ├── MultiAccountPublishingViewModel.kt │ │ │ │ │ └── PublishingAccounts.kt │ │ │ └── status │ │ │ │ └── context │ │ │ │ ├── StatusContextScreen.kt │ │ │ │ ├── StatusContextSubViewModel.kt │ │ │ │ ├── StatusContextUiState.kt │ │ │ │ └── StatusContextViewModel.kt │ │ │ ├── usecase │ │ │ ├── PublishPostOnMultiAccountUseCase.kt │ │ │ ├── RefactorToNewBlogUseCase.kt │ │ │ └── RefactorToNewStatusUseCase.kt │ │ │ └── utils │ │ │ └── LoadableStatusController.kt │ │ └── iosMain │ │ └── kotlin │ │ └── com │ │ └── zhangke │ │ └── fread │ │ └── commonbiz │ │ └── shared │ │ ├── SharedScreenPlatformModule.ios.kt │ │ ├── composable │ │ ├── OnBlogMediaClick.ios.kt │ │ └── WebViewPreviewer.ios.kt │ │ └── screen │ │ └── ImageViewerScreen.ios.kt └── status-ui │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ ├── androidMain │ └── kotlin │ │ └── com │ │ └── zhangke │ │ └── fread │ │ └── status │ │ └── ui │ │ ├── StatusPlaceHolder.preview.kt │ │ ├── common │ │ └── FormattingTimeText.kt │ │ ├── poll │ │ └── BlogPollOption.preview.kt │ │ ├── source │ │ └── SourceCommonUi.preview.kt │ │ ├── utils │ │ └── ScreenSize.android.kt │ │ └── video │ │ ├── BlogVideos.kt │ │ ├── LocalInlineVideoPlayer.kt │ │ ├── VideoDurationFormatter.kt │ │ ├── full │ │ └── FullScreenVideoPlayer.kt │ │ └── inline │ │ └── InlineVideo.kt │ ├── commonMain │ ├── composeResources │ │ ├── drawable │ │ │ ├── ic_drag_indicator.xml │ │ │ ├── ic_format_quote.xml │ │ │ ├── ic_mode_edit.xml │ │ │ ├── ic_more.xml │ │ │ ├── ic_post_status_spoiler.xml │ │ │ ├── ic_share.xml │ │ │ ├── ic_status_comment.xml │ │ │ ├── ic_status_forward.xml │ │ │ ├── img_banner_background.xml │ │ │ └── status_ui_baseline_visibility_off_24.xml │ │ ├── values-zh │ │ │ └── strings.xml │ │ └── values │ │ │ └── strings.xml │ └── kotlin │ │ ├── com │ │ └── zhangke │ │ │ └── fread │ │ │ └── status │ │ │ └── ui │ │ │ ├── BlogAuthorAvatar.kt │ │ │ ├── BlogAuthorUi.kt │ │ │ ├── BlogContent.kt │ │ │ ├── BlogDivider.kt │ │ │ ├── BlogUi.kt │ │ │ ├── ComposedStatusInteraction.kt │ │ │ ├── StatusInfoLine.kt │ │ │ ├── StatusPlaceHolder.kt │ │ │ ├── StatusUi.kt │ │ │ ├── action │ │ │ ├── ModalDropdownMenuItem.kt │ │ │ ├── StatusActions.kt │ │ │ ├── StatusBottomInteractionPanel.kt │ │ │ ├── StatusIconButton.kt │ │ │ ├── StatusInteractiveExts.kt │ │ │ └── StatusMoreInteractionPanel.kt │ │ │ ├── bar │ │ │ └── EditContentTopBar.kt │ │ │ ├── common │ │ │ ├── BlogTranslaction.kt │ │ │ ├── ContentToolbar.kt │ │ │ ├── NestedTabConnection.kt │ │ │ ├── NewStatusNotifyBar.kt │ │ │ ├── ObserveMaxReadItem.kt │ │ │ ├── PostStatusTextVisualTransformation.kt │ │ │ ├── ProgressedAvatar.kt │ │ │ ├── ProgressedBanner.kt │ │ │ ├── RelationshipStateButton.kt │ │ │ ├── RemainingTextStatus.kt │ │ │ ├── SelectAccountDialog.kt │ │ │ └── UserFollowLine.kt │ │ │ ├── embed │ │ │ ├── BlogEmbedsUi.kt │ │ │ ├── BlogInEmbedding.kt │ │ │ └── StatusEmbedLinkUi.kt │ │ │ ├── hashtag │ │ │ └── HashtagUi.kt │ │ │ ├── image │ │ │ ├── BlogImageMedia.kt │ │ │ ├── DoubleBlogImageLayout.kt │ │ │ ├── FivefoldImageMediaFrameLayout.kt │ │ │ ├── HorizontalImageMediaFrameLayout.kt │ │ │ ├── HorizontalImageMediaListLayout.kt │ │ │ ├── QuadrupleImageMediaLayout.kt │ │ │ ├── SingleBlogImageLayout.kt │ │ │ ├── SixfoldImageMediaLayout.kt │ │ │ ├── TripleImageMediaLayout.kt │ │ │ ├── VerticalImageMediaFrameLayout.kt │ │ │ └── VerticalImageMediaListLayout.kt │ │ │ ├── label │ │ │ ├── StatusBottomEditedLabel.kt │ │ │ ├── StatusBottomInteractionLabel.kt │ │ │ ├── StatusBottomTimeLabel.kt │ │ │ └── StatusTopLabel.kt │ │ │ ├── media │ │ │ └── BlogMedias.kt │ │ │ ├── placeholder │ │ │ └── ListWithAvatarPlaceholder.kt │ │ │ ├── poll │ │ │ ├── BlogPoll.kt │ │ │ ├── BlogPollOption.kt │ │ │ ├── MultipleChoicePoll.kt │ │ │ └── SingleChoicePoll.kt │ │ │ ├── publish │ │ │ ├── BlogInQuoting.kt │ │ │ ├── NameAndAccountInfo.kt │ │ │ └── PublishBlogStyle.kt │ │ │ ├── richtext │ │ │ └── FreadRichText.kt │ │ │ ├── source │ │ │ ├── BlogPlatformUi.kt │ │ │ ├── SearchContentResultUi.kt │ │ │ ├── SourceCommonUi.kt │ │ │ └── StatusSourceUi.kt │ │ │ ├── style │ │ │ ├── LocalStatusStyle.kt │ │ │ ├── StatusInfoStyle.kt │ │ │ ├── StatusStyle.kt │ │ │ └── StatusUiConfig.kt │ │ │ ├── threads │ │ │ ├── Threads.kt │ │ │ └── ThreadsType.kt │ │ │ ├── update │ │ │ └── AppUpdateDialog.kt │ │ │ ├── user │ │ │ └── CommonUserUi.kt │ │ │ ├── utils │ │ │ ├── CardInfoSection.kt │ │ │ └── ScreenSize.kt │ │ │ └── video │ │ │ └── BlogVideos.kt │ │ └── org │ │ └── burnoutcrew │ │ └── reorderable │ │ ├── DetectReorder.kt │ │ ├── DragCancelledAnimation.kt │ │ ├── DragGesture.kt │ │ ├── ItemPosition.kt │ │ ├── Reorderable.kt │ │ ├── ReorderableItem.kt │ │ ├── ReorderableLazyGridState.kt │ │ ├── ReorderableLazyListState.kt │ │ └── ReorderableState.kt │ └── iosMain │ └── kotlin │ └── com │ └── zhangke │ └── fread │ └── status │ └── ui │ ├── utils │ └── ScreenSize.ios.kt │ └── video │ └── BlogVideos.ios.kt ├── deleteuserdata.html ├── documents ├── UserSource.drawio └── architecture.xmind ├── fastlane ├── Fastfile └── metadata │ └── android │ ├── en-US │ ├── full_description.txt │ └── short_description.txt │ └── zh-CN │ ├── full_description.txt │ └── short_description.txt ├── feature ├── explore │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ │ └── commonMain │ │ ├── composeResources │ │ ├── values-zh │ │ │ └── strings.xml │ │ └── values │ │ │ └── strings.xml │ │ └── kotlin │ │ └── com │ │ └── zhangke │ │ └── fread │ │ └── explore │ │ ├── ExploreTab.kt │ │ ├── ExplorerElements.kt │ │ ├── di │ │ └── ExploreComponent.kt │ │ ├── model │ │ └── ExplorerItem.kt │ │ ├── screens │ │ ├── home │ │ │ ├── ExplorerHomeScreen.kt │ │ │ ├── ExplorerHomeUiState.kt │ │ │ └── ExplorerHomeViewModel.kt │ │ └── search │ │ │ ├── SearchScreen.kt │ │ │ ├── SearchUiState.kt │ │ │ ├── SearchViewModel.kt │ │ │ ├── author │ │ │ ├── SearchAuthorViewModel.kt │ │ │ └── SearchedAuthorTab.kt │ │ │ ├── bar │ │ │ ├── ExplorerSearchBar.kt │ │ │ ├── SearchBarUiState.kt │ │ │ └── SearchBarViewModel.kt │ │ │ ├── hashtag │ │ │ ├── SearchHashtagViewModel.kt │ │ │ └── SearchedHashtagTab.kt │ │ │ ├── platform │ │ │ ├── SearchPlatformViewModel.kt │ │ │ ├── SearchedPlatformTab.kt │ │ │ └── SearchedPlatformUiState.kt │ │ │ └── status │ │ │ ├── SearchStatusViewModel.kt │ │ │ └── SearchedStatusTab.kt │ │ └── usecase │ │ └── BuildSearchResultUiStateUseCase.kt ├── feeds │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ │ ├── androidMain │ │ └── kotlin │ │ │ └── com │ │ │ └── zhangke │ │ │ └── fread │ │ │ └── feeds │ │ │ └── pages │ │ │ └── manager │ │ │ └── importing │ │ │ └── OpenDocumentContainer.android.kt │ │ ├── commonMain │ │ ├── composeResources │ │ │ ├── drawable │ │ │ │ ├── ic_home.xml │ │ │ │ └── ic_import.xml │ │ │ ├── values-zh │ │ │ │ └── strings.xml │ │ │ └── values │ │ │ │ └── strings.xml │ │ └── kotlin │ │ │ └── com │ │ │ └── zhangke │ │ │ └── fread │ │ │ └── feeds │ │ │ ├── FeedsHomeTab.kt │ │ │ ├── FeedsScreenVisitor.kt │ │ │ ├── composable │ │ │ └── StatusSource.kt │ │ │ ├── di │ │ │ └── FeedsComponent.kt │ │ │ └── pages │ │ │ ├── home │ │ │ ├── ContentHomeScreen.kt │ │ │ ├── ContentHomeUiState.kt │ │ │ ├── ContentHomeViewModel.kt │ │ │ ├── EmptyContent.kt │ │ │ ├── SelectAccountForPostStatusScreen.kt │ │ │ └── feeds │ │ │ │ ├── MixedContentScreen.kt │ │ │ │ ├── MixedContentSubViewModel.kt │ │ │ │ ├── MixedContentUiState.kt │ │ │ │ └── MixedContentViewModel.kt │ │ │ └── manager │ │ │ ├── add │ │ │ ├── mixed │ │ │ │ ├── AddMixedFeedsScreen.kt │ │ │ │ ├── AddMixedFeedsUiState.kt │ │ │ │ └── AddMixedFeedsViewModel.kt │ │ │ └── pre │ │ │ │ ├── PreAddFeedsScreen.kt │ │ │ │ ├── PreAddFeedsUiState.kt │ │ │ │ └── PreAddFeedsViewModel.kt │ │ │ ├── edit │ │ │ ├── EditMixedContentScreen.kt │ │ │ ├── EditMixedContentUiState.kt │ │ │ └── EditMixedContentViewModel.kt │ │ │ ├── importing │ │ │ ├── ImportFeedsScreen.kt │ │ │ ├── ImportFeedsUiState.kt │ │ │ ├── ImportFeedsViewModel.kt │ │ │ └── OpenDocumentContainer.kt │ │ │ └── search │ │ │ ├── SearchSourceForAddScreen.kt │ │ │ └── SearchSourceForAddViewModel.kt │ │ └── iosMain │ │ └── kotlin │ │ └── com │ │ └── zhangke │ │ └── fread │ │ └── feeds │ │ └── pages │ │ └── manager │ │ └── importing │ │ └── OpenDocumentContainer.ios.kt ├── notifications │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ │ ├── androidMain │ │ └── kotlin │ │ │ └── com │ │ │ └── zhangke │ │ │ └── fread │ │ │ └── feature │ │ │ └── message │ │ │ └── di │ │ │ └── NotificationsComponentPlatform.android.kt │ │ ├── commonMain │ │ ├── composeResources │ │ │ ├── drawable │ │ │ │ └── ic_notification_tab.xml │ │ │ ├── values-zh │ │ │ │ └── strings.xml │ │ │ └── values │ │ │ │ └── strings.xml │ │ └── kotlin │ │ │ └── com │ │ │ └── zhangke │ │ │ └── fread │ │ │ └── feature │ │ │ └── message │ │ │ ├── NotificationElements.kt │ │ │ ├── NotificationsTab.kt │ │ │ ├── di │ │ │ └── NotificationsComponent.kt │ │ │ ├── repo │ │ │ └── notification │ │ │ │ ├── NotificationsDatabase.kt │ │ │ │ └── NotificationsRepo.kt │ │ │ └── screens │ │ │ ├── home │ │ │ ├── NotificationsHomeScreen.kt │ │ │ ├── NotificationsHomeUiState.kt │ │ │ └── NotificationsHomeViewModel.kt │ │ │ └── notification │ │ │ ├── NotificationContainerViewModel.kt │ │ │ ├── NotificationTab.kt │ │ │ ├── NotificationUiState.kt │ │ │ └── NotificationViewModel.kt │ │ └── iosMain │ │ └── kotlin │ │ └── com │ │ └── zhangke │ │ └── fread │ │ └── feature │ │ └── message │ │ └── di │ │ └── NotificationsComponentPlatform.ios.kt └── profile │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ └── commonMain │ ├── composeResources │ ├── drawable │ │ ├── af_dian.webp │ │ ├── ic_code.xml │ │ ├── ic_github_logo.xml │ │ ├── ic_profile_tab.xml │ │ ├── ic_ratting.xml │ │ ├── ic_telegram.xml │ │ └── kofi_symbol.xml │ ├── values-zh │ │ └── strings.xml │ └── values │ │ └── strings.xml │ └── kotlin │ └── com │ └── zhangke │ └── fread │ └── profile │ ├── ProfileScreenVisitor.kt │ ├── ProfileTab.kt │ ├── di │ └── ProfileComponent.kt │ └── screen │ ├── donate │ └── DonateScreen.kt │ ├── home │ ├── ProfileHomePage.kt │ ├── ProfileHomeUiState.kt │ └── ProfileHomeViewModel.kt │ ├── opensource │ └── OpenSourceScreen.kt │ └── setting │ ├── FeedbackBottomSheet.kt │ ├── SettingScreen.kt │ ├── SettingScreenModel.kt │ ├── SettingUiState.kt │ └── about │ ├── AboutScreen.kt │ ├── AboutUiState.kt │ └── AboutViewModel.kt ├── framework ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidInstrumentedTest │ └── kotlin │ │ └── com │ │ └── zhangke │ │ └── framework │ │ ├── ExampleInstrumentedTest.kt │ │ └── utils │ │ └── UtiUtilsTest.kt │ ├── androidMain │ └── kotlin │ │ └── com │ │ └── zhangke │ │ └── framework │ │ ├── activity │ │ └── TopActivityManager.kt │ │ ├── architect │ │ ├── http │ │ │ ├── GlobalOkHttpClient.kt │ │ │ └── HttpClientEngine.android.kt │ │ └── theme │ │ │ └── FreadTheme.android.kt │ │ ├── blurhash │ │ └── BitmapExt.android.kt │ │ ├── composable │ │ ├── AnimatableExt.kt │ │ ├── Loading.android.kt │ │ ├── TextString.android.kt │ │ ├── pick │ │ │ └── PickVisualMediaLauncherContainer.android.kt │ │ └── video │ │ │ ├── ExoPlayerManager.kt │ │ │ └── VideoPlayer.kt │ │ ├── date │ │ └── InstantFormater.android.kt │ │ ├── datetime │ │ └── Instant.android.kt │ │ ├── ktx │ │ └── SingletonDelegate.kt │ │ ├── media │ │ └── MediaFileUtil.kt │ │ ├── permission │ │ ├── PermissionUtils.kt │ │ ├── RequireLocalStoragePermission.android.kt │ │ └── RequirePermission.kt │ │ ├── serialize │ │ └── DateSerializer.kt │ │ ├── toast │ │ └── Toast.android.kt │ │ └── utils │ │ ├── ActivityLifecycleCallbacksAdapter.kt │ │ ├── ActivityResultContractsExt.kt │ │ ├── BitmapUtils.kt │ │ ├── ContextUtils.kt │ │ ├── DensityUtils.android.kt │ │ ├── DrawableExt.kt │ │ ├── DrawableWrapper.kt │ │ ├── ExoPlayerUtils.kt │ │ ├── FileUtils.kt │ │ ├── ImageCompressUtils.android.kt │ │ ├── ImageLoaderUtils.kt │ │ ├── LanguageUtils.android.kt │ │ ├── PlatformIgnoredOnParcel.android.kt │ │ ├── PlatformParcelable.android.kt │ │ ├── PlatformTransient.android.kt │ │ ├── PlatformUri.android.kt │ │ ├── Serializable.android.kt │ │ ├── SignatureUtils.kt │ │ ├── SystemPageUtils.kt │ │ ├── SystemUtils.kt │ │ ├── UriUtils.android.kt │ │ └── VideoUtils.android.kt │ ├── androidUnitTest │ └── kotlin │ │ └── com │ │ └── zhangke │ │ └── framework │ │ ├── ExampleUnitTest.kt │ │ ├── RegexFactoryTest.kt │ │ ├── date │ │ └── DateParserTest.kt │ │ ├── feeds │ │ └── fetcher │ │ │ ├── FeedsFetcherTest.kt │ │ │ ├── FeedsGeneratorTest.kt │ │ │ ├── StatusDataSourceTest.kt │ │ │ └── StatusPagingSourceTest.kt │ │ └── utils │ │ ├── RegexFactoryTest.kt │ │ ├── StorageSizeTest.kt │ │ └── WebFingerTest.kt │ ├── commonMain │ ├── composeResources │ │ ├── values-zh │ │ │ └── strings.xml │ │ └── values │ │ │ └── strings.xml │ ├── kotlin │ │ └── com │ │ │ ├── sd │ │ │ └── lib │ │ │ │ └── compose │ │ │ │ └── wheel_picker │ │ │ │ ├── WheelPicker.kt │ │ │ │ ├── WheelPickerDefault.kt │ │ │ │ └── WheelPickerState.kt │ │ │ └── zhangke │ │ │ └── framework │ │ │ ├── architect │ │ │ ├── coroutines │ │ │ │ ├── ApplicationScope.kt │ │ │ │ └── Flows.kt │ │ │ ├── http │ │ │ │ └── HttpClientEngine.kt │ │ │ ├── json │ │ │ │ ├── Json.kt │ │ │ │ └── JsonModuleBuilder.kt │ │ │ └── theme │ │ │ │ ├── Color.kt │ │ │ │ └── FreadTheme.kt │ │ │ ├── blurhash │ │ │ ├── BitmapExt.kt │ │ │ ├── BlurHashDecoder.kt │ │ │ └── BlurHashModifier.kt │ │ │ ├── collections │ │ │ └── Collections.kt │ │ │ ├── composable │ │ │ ├── AlertConfirmDialog.kt │ │ │ ├── AppBar.kt │ │ │ ├── AvatarStatck.kt │ │ │ ├── BezierCurve.kt │ │ │ ├── BottomSheetDialog.kt │ │ │ ├── Bounds.kt │ │ │ ├── CollapsableTopBarScrollConnection.kt │ │ │ ├── DatePickerDialog.kt │ │ │ ├── DirectionalLazyListState.kt │ │ │ ├── DpSaver.kt │ │ │ ├── DurationSelector.kt │ │ │ ├── FlowUtils.android.kt │ │ │ ├── FlowUtils.kt │ │ │ ├── FreadDialog.kt │ │ │ ├── FreadTabRow.kt │ │ │ ├── Grid.kt │ │ │ ├── HorizontalPageIndicator.kt │ │ │ ├── IconButton.kt │ │ │ ├── Keyboard.kt │ │ │ ├── LazyListStateUtils.kt │ │ │ ├── LoadableLayout.kt │ │ │ ├── Loading.kt │ │ │ ├── LoadingDialog.kt │ │ │ ├── LocalSnackMessage.kt │ │ │ ├── NavigationBar.kt │ │ │ ├── NestedScrollConnection.kt │ │ │ ├── NoDoubleClick.kt │ │ │ ├── NoRippleClick.kt │ │ │ ├── Offset.kt │ │ │ ├── PaddingValuesUtils.kt │ │ │ ├── PagerTab.kt │ │ │ ├── Placeholder.kt │ │ │ ├── PopupFloatingActionButton.kt │ │ │ ├── ScreenUtils.kt │ │ │ ├── SearchToolbar.kt │ │ │ ├── Size.kt │ │ │ ├── SlickRoundCornerShape.kt │ │ │ ├── Snackbar.kt │ │ │ ├── StateSaver.kt │ │ │ ├── StyledIconButton.kt │ │ │ ├── StyledTextButton.kt │ │ │ ├── SystemUi.kt │ │ │ ├── TabIndicator.kt │ │ │ ├── TextString.kt │ │ │ ├── TextWithIcon.kt │ │ │ ├── Toolbar.kt │ │ │ ├── TopBarWithTabLayout.kt │ │ │ ├── TwoTextsInRow.kt │ │ │ ├── VelocityExt.kt │ │ │ ├── collapsable │ │ │ │ ├── CollapsableTopBarLayout.kt │ │ │ │ ├── CollapsableTopBarLayoutConnection.kt │ │ │ │ └── ScrollUpTopBarLayout.kt │ │ │ ├── icons │ │ │ │ └── Toufu.kt │ │ │ ├── image │ │ │ │ └── viewer │ │ │ │ │ ├── ImageViewer.kt │ │ │ │ │ ├── ImageViewerState.kt │ │ │ │ │ └── TransformGestureDetector.kt │ │ │ ├── infinite │ │ │ │ ├── InfiniteBox.kt │ │ │ │ └── InfinityBoxState.kt │ │ │ ├── inline │ │ │ │ ├── InlineVideoLazyColumn.kt │ │ │ │ └── PlayableIndexRecorderLocal.kt │ │ │ ├── pick │ │ │ │ └── PickVisualMediaLauncherContainer.kt │ │ │ ├── sensitive │ │ │ │ ├── SensitiveLazyColumn.kt │ │ │ │ └── SensitiveLazyColumnState.kt │ │ │ ├── theme │ │ │ │ └── TopAppBar.kt │ │ │ ├── topout │ │ │ │ ├── TopOutTopBarLayout.kt │ │ │ │ └── TopOutTopBarLayoutConnection.kt │ │ │ └── video │ │ │ │ ├── VideoPlayer.kt │ │ │ │ └── VideoState.kt │ │ │ ├── controller │ │ │ ├── CommonLoadableController.kt │ │ │ └── LoadableController.kt │ │ │ ├── coroutines │ │ │ └── JobExt.kt │ │ │ ├── date │ │ │ ├── DateParser.kt │ │ │ └── InstantFormater.kt │ │ │ ├── datetime │ │ │ └── Instant.kt │ │ │ ├── imageloader │ │ │ └── ImageLoaderUtils.kt │ │ │ ├── ktx │ │ │ ├── CollectionsExt.kt │ │ │ ├── FlowExt.kt │ │ │ ├── LazyBackingFieldDelegate.kt │ │ │ ├── StringExt.kt │ │ │ └── ViewModels.kt │ │ │ ├── lifecycle │ │ │ ├── ContainerViewModel.kt │ │ │ └── SubViewModel.kt │ │ │ ├── loadable │ │ │ ├── lazycolumn │ │ │ │ ├── LoadMoreUi.kt │ │ │ │ ├── LoadableInlineVideoLazyColumn.kt │ │ │ │ └── LoadableLazyColumn.kt │ │ │ └── previous │ │ │ │ └── PreviousPageLoadingState.kt │ │ │ ├── module │ │ │ └── ModuleStartup.kt │ │ │ ├── network │ │ │ ├── FormalBaseUrl.kt │ │ │ ├── GlobalRoutes.kt │ │ │ ├── HttpScheme.kt │ │ │ └── SimpleUri.kt │ │ │ ├── opml │ │ │ ├── OpmlOutline.kt │ │ │ └── OpmlParser.kt │ │ │ ├── permission │ │ │ └── RequireLocalStoragePermission.kt │ │ │ ├── security │ │ │ └── Md5.kt │ │ │ ├── serialize │ │ │ └── TimestampAsInstantSerializer.kt │ │ │ ├── toast │ │ │ └── Toast.kt │ │ │ ├── utils │ │ │ ├── AspectRatio.kt │ │ │ ├── BlendColorUtils.kt │ │ │ ├── ContentProviderFile.kt │ │ │ ├── DebugUtils.kt │ │ │ ├── DensityUtils.kt │ │ │ ├── DurationFormatUtils.kt │ │ │ ├── FloatExt.kt │ │ │ ├── Handle.kt │ │ │ ├── HighlightTextBuildUtil.kt │ │ │ ├── ImageCompressUtils.kt │ │ │ ├── IntExt.kt │ │ │ ├── LanguageUtils.kt │ │ │ ├── LoadState.kt │ │ │ ├── Log.kt │ │ │ ├── Parcelize.kt │ │ │ ├── PlatformIgnoredOnParcel.kt │ │ │ ├── PlatformParcelable.kt │ │ │ ├── PlatformSerializable.kt │ │ │ ├── PlatformTransient.kt │ │ │ ├── PlatformUri.kt │ │ │ ├── RegexFactory.kt │ │ │ ├── ResultExt.kt │ │ │ ├── Rfc822InstantParser.kt │ │ │ ├── Standard.kt │ │ │ ├── StorageSize.kt │ │ │ ├── TextFieldUtils.kt │ │ │ ├── UriUtils.kt │ │ │ ├── UrlEncoder.kt │ │ │ ├── VideoUtils.kt │ │ │ └── WebFinger.kt │ │ │ └── voyager │ │ │ ├── RootNavigator.kt │ │ │ ├── TransparentNavigator.kt │ │ │ └── VoyagerResultExtension.kt │ └── res │ │ └── values │ │ ├── colors.xml │ │ ├── ids.xml │ │ └── themes.xml │ ├── commonTest │ └── kotlin │ │ └── com │ │ └── zhangke │ │ └── framework │ │ ├── network │ │ └── SimpleUriTest.kt │ │ ├── opml │ │ └── OpmlParserTest.kt │ │ ├── security │ │ └── Md5Test.kt │ │ └── utils │ │ └── IntExtTest.kt │ └── iosMain │ └── kotlin │ └── com │ └── zhangke │ └── framework │ ├── architect │ ├── http │ │ └── HttpClientEngine.ios.kt │ └── theme │ │ └── FreadTheme.ios.kt │ ├── blurhash │ └── BitmapExt.ios.kt │ ├── composable │ ├── TextString.ios.kt │ ├── pick │ │ └── PickVisualMediaLauncherContainer.ios.kt │ └── video │ │ └── VideoPlayer.ios.kt │ ├── date │ └── InstantFormater.ios.kt │ ├── datetime │ └── Instant.ios.kt │ ├── permission │ └── RequireLocalStoragePermission.ios.kt │ ├── toast │ └── Toast.ios.kt │ └── utils │ ├── ImageCompressUtils.kt │ ├── LanguageUtils.ios.kt │ ├── PlatformIgnoredOnParcel.ios.kt │ ├── PlatformParcelable.ios.kt │ ├── PlatformTransient.ios.kt │ ├── PlatformUri.ios.kt │ ├── Serializable.ios.kt │ └── VideoUtils.ios.kt ├── google-play-download.png ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── ic_download_apk.png ├── iosApp ├── .gitignore ├── Configuration │ └── Config.xcconfig ├── iosApp.xcodeproj │ └── project.pbxproj └── iosApp │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ └── app-icon-1024.png │ └── Contents.json │ ├── ContentView.swift │ ├── GoogleService-Info.plist │ ├── Info.plist │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ └── iOSApp.swift ├── plugins ├── activitypub-app │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ │ ├── androidMain │ │ ├── AndroidManifest.xml │ │ └── kotlin │ │ │ └── com │ │ │ └── zhangke │ │ │ └── fread │ │ │ └── activitypub │ │ │ └── app │ │ │ ├── di │ │ │ └── ActivityPubComponent.android.kt │ │ │ └── internal │ │ │ ├── auth │ │ │ └── ActivityPubOAuthRedirectActivity.kt │ │ │ └── push │ │ │ ├── ActivityPubPushManager.android.kt │ │ │ ├── ActivityPubPushMessageReceiverHelper.android.kt │ │ │ ├── CryptoUtil.kt │ │ │ ├── PushInfoRepo.kt │ │ │ └── notification │ │ │ ├── ActivityPubPushMessage.kt │ │ │ └── PushNotificationManager.kt │ │ ├── androidUnitTest │ │ └── kotlin │ │ │ └── com │ │ │ └── zhangke │ │ │ └── utopia │ │ │ └── activitypubapp │ │ │ └── ExampleUnitTest.kt │ │ ├── commonMain │ │ ├── composeResources │ │ │ ├── drawable │ │ │ │ └── detail_page_banner_background.xml │ │ │ ├── files │ │ │ │ └── mastodon-servers.zip │ │ │ ├── values-zh │ │ │ │ └── strings.xml │ │ │ └── values │ │ │ │ └── strings.xml │ │ └── kotlin │ │ │ └── com │ │ │ └── zhangke │ │ │ └── fread │ │ │ └── activitypub │ │ │ └── app │ │ │ ├── ActivityPubAccountManager.kt │ │ │ ├── ActivityPubContentManager.kt │ │ │ ├── ActivityPubJsonBuilder.kt │ │ │ ├── ActivityPubNotificationResolver.kt │ │ │ ├── ActivityPubPlatformResolver.kt │ │ │ ├── ActivityPubProtocol.kt │ │ │ ├── ActivityPubProvider.kt │ │ │ ├── ActivityPubPublishManager.kt │ │ │ ├── ActivityPubScreenProvider.kt │ │ │ ├── ActivityPubSearchEngine.kt │ │ │ ├── ActivityPubSourceResolver.kt │ │ │ ├── ActivityPubStartup.kt │ │ │ ├── ActivityPubStatusResolver.kt │ │ │ ├── ActivityPubUrlInterceptor.kt │ │ │ ├── di │ │ │ └── ActivityPubComponent.kt │ │ │ └── internal │ │ │ ├── DataTrackingElements.kt │ │ │ ├── adapter │ │ │ ├── ActivityPubAccountEntityAdapter.kt │ │ │ ├── ActivityPubApplicationEntityAdapter.kt │ │ │ ├── ActivityPubBlogMetaAdapter.kt │ │ │ ├── ActivityPubContentAdapter.kt │ │ │ ├── ActivityPubCustomEmojiEntityAdapter.kt │ │ │ ├── ActivityPubInstanceAdapter.kt │ │ │ ├── ActivityPubLoggedAccountAdapter.kt │ │ │ ├── ActivityPubPlatformEntityAdapter.kt │ │ │ ├── ActivityPubPollAdapter.kt │ │ │ ├── ActivityPubSearchAdapter.kt │ │ │ ├── ActivityPubStatusAdapter.kt │ │ │ ├── ActivityPubTagAdapter.kt │ │ │ ├── ActivityPubTranslationEntityAdapter.kt │ │ │ ├── PostStatusAttachmentAdapter.kt │ │ │ └── RegisterApplicationEntryAdapter.kt │ │ │ ├── auth │ │ │ ├── ActivityPubClientManager.kt │ │ │ ├── ActivityPubOAuthor.kt │ │ │ └── LoggedAccountProvider.kt │ │ │ ├── composable │ │ │ └── ActivityPubTabNames.kt │ │ │ ├── content │ │ │ └── ActivityPubContent.kt │ │ │ ├── db │ │ │ ├── ActivityPubApplicationTable.kt │ │ │ ├── ActivityPubDatabases.kt │ │ │ ├── ActivityPubLoggedAccountTable.kt │ │ │ ├── ActivityPubPlatformTable.kt │ │ │ ├── UserIdTable.kt │ │ │ ├── converter │ │ │ │ ├── ActivityPubAccountEntityConverter.kt │ │ │ │ ├── ActivityPubInstanceEntityConverter.kt │ │ │ │ ├── ActivityPubStatusEntityConverter.kt │ │ │ │ ├── ActivityPubStatusSourceTypeConverter.kt │ │ │ │ ├── ActivityPubUserTokenConverter.kt │ │ │ │ ├── BlogAuthorConverter.kt │ │ │ │ ├── EmojiListConverter.kt │ │ │ │ ├── FormalBaseUrlConverter.kt │ │ │ │ ├── PlatformEntityTypeConverter.kt │ │ │ │ ├── RelationshipSeveranceEventConverter.kt │ │ │ │ └── StatusNotificationTypeConverter.kt │ │ │ └── status │ │ │ │ ├── ActivityPubStatusDatabases.kt │ │ │ │ └── ActivityPubStatusReadStateDatabases.kt │ │ │ ├── model │ │ │ ├── ActivityPubApplication.kt │ │ │ ├── ActivityPubInstanceRule.kt │ │ │ ├── ActivityPubLoggedAccount.kt │ │ │ ├── ActivityPubStatusSourceType.kt │ │ │ ├── ActivityPubTimelineType.kt │ │ │ ├── CustomEmoji.kt │ │ │ ├── PlatformUriInsights.kt │ │ │ ├── RelationshipSeveranceEvent.kt │ │ │ ├── ServerDetailContract.kt │ │ │ ├── StatusNotification.kt │ │ │ ├── StatusNotificationType.kt │ │ │ ├── UserSource.kt │ │ │ └── UserUriInsights.kt │ │ │ ├── platform │ │ │ └── FreadApplicationRegisterInfo.kt │ │ │ ├── push │ │ │ ├── ActivityPubPushManager.kt │ │ │ └── ActivityPubPushMessageReceiver.kt │ │ │ ├── repo │ │ │ ├── WebFingerBaseUrlToUserIdRepo.kt │ │ │ ├── account │ │ │ │ └── ActivityPubLoggedAccountRepo.kt │ │ │ ├── application │ │ │ │ └── ActivityPubApplicationRepo.kt │ │ │ ├── platform │ │ │ │ ├── ActivityPubPlatformRepo.kt │ │ │ │ ├── BlogPlatformResourceLoader.kt │ │ │ │ └── MastodonInstanceRepo.kt │ │ │ ├── status │ │ │ │ ├── ActivityPubStatusReadStateRepo.kt │ │ │ │ └── ActivityPubTimelineStatusRepo.kt │ │ │ └── user │ │ │ │ └── UserRepo.kt │ │ │ ├── route │ │ │ └── ActivityPubRoutes.kt │ │ │ ├── screen │ │ │ ├── account │ │ │ │ ├── EditAccountInfoScreen.kt │ │ │ │ ├── EditAccountInfoViewModel.kt │ │ │ │ └── EditAccountUiState.kt │ │ │ ├── add │ │ │ │ ├── AddActivityPubContentScreen.kt │ │ │ │ ├── AddActivityPubContentUiState.kt │ │ │ │ └── AddActivityPubContentViewModel.kt │ │ │ ├── content │ │ │ │ ├── ActivityPubContentScreen.kt │ │ │ │ ├── ActivityPubContentSubViewModel.kt │ │ │ │ ├── ActivityPubContentUiState.kt │ │ │ │ ├── ActivityPubContentViewModel.kt │ │ │ │ ├── edit │ │ │ │ │ ├── EditContentConfigScreen.kt │ │ │ │ │ ├── EditContentConfigUiState.kt │ │ │ │ │ └── EditContentConfigViewModel.kt │ │ │ │ └── timeline │ │ │ │ │ ├── ActivityPubTimelineContainerViewModel.kt │ │ │ │ │ ├── ActivityPubTimelineTab.kt │ │ │ │ │ ├── ActivityPubTimelineViewModel.kt │ │ │ │ │ └── ActivityPubUiState.kt │ │ │ ├── explorer │ │ │ │ ├── ExplorerContainerTab.kt │ │ │ │ ├── ExplorerContainerViewModel.kt │ │ │ │ ├── ExplorerFeedsTabType.kt │ │ │ │ ├── ExplorerItem.kt │ │ │ │ ├── ExplorerTab.kt │ │ │ │ ├── ExplorerUiState.kt │ │ │ │ └── ExplorerViewModel.kt │ │ │ ├── filters │ │ │ │ ├── edit │ │ │ │ │ ├── EditFilterScreen.kt │ │ │ │ │ ├── EditFilterUiState.kt │ │ │ │ │ ├── EditFilterViewModel.kt │ │ │ │ │ ├── FilterContext.kt │ │ │ │ │ └── HiddenKeywordScreen.kt │ │ │ │ └── list │ │ │ │ │ ├── FiltersListScreen.kt │ │ │ │ │ ├── FiltersListUiState.kt │ │ │ │ │ └── FiltersListViewModel.kt │ │ │ ├── hashtag │ │ │ │ ├── HashtagTimelineContainerViewModel.kt │ │ │ │ ├── HashtagTimelineRoute.kt │ │ │ │ ├── HashtagTimelineScreen.kt │ │ │ │ ├── HashtagTimelineUiState.kt │ │ │ │ └── HashtagTimelineViewModel.kt │ │ │ ├── instance │ │ │ │ ├── InstanceDetailScreen.kt │ │ │ │ ├── InstanceDetailTab.kt │ │ │ │ ├── InstanceDetailUiState.kt │ │ │ │ ├── InstanceDetailViewModel.kt │ │ │ │ ├── PlatformDetailRoute.kt │ │ │ │ ├── about │ │ │ │ │ ├── ServerAboutPage.kt │ │ │ │ │ ├── ServerAboutUiState.kt │ │ │ │ │ └── ServerAboutViewModel.kt │ │ │ │ └── tags │ │ │ │ │ ├── ServerTrendsTagsPage.kt │ │ │ │ │ ├── ServerTrendsTagsUiState.kt │ │ │ │ │ └── ServerTrendsTagsViewModel.kt │ │ │ ├── list │ │ │ │ ├── CreatedListsScreen.kt │ │ │ │ ├── CreatedListsUiState.kt │ │ │ │ ├── CreatedListsViewModel.kt │ │ │ │ ├── ListDetailPageContent.kt │ │ │ │ ├── ListItem.kt │ │ │ │ ├── add │ │ │ │ │ ├── AddListScreen.kt │ │ │ │ │ ├── AddListUiState.kt │ │ │ │ │ └── AddListViewModel.kt │ │ │ │ └── edit │ │ │ │ │ ├── EditListScreen.kt │ │ │ │ │ ├── EditListUiState.kt │ │ │ │ │ └── EditListViewModel.kt │ │ │ ├── status │ │ │ │ └── post │ │ │ │ │ ├── InputMediaDescriptionScreen.kt │ │ │ │ │ ├── PostStatusScreen.kt │ │ │ │ │ ├── PostStatusScreenRoute.kt │ │ │ │ │ ├── PostStatusUiState.kt │ │ │ │ │ ├── PostStatusViewModel.kt │ │ │ │ │ ├── adapter │ │ │ │ │ └── CustomEmojiAdapter.kt │ │ │ │ │ ├── composable │ │ │ │ │ ├── CustomEmojiPicker.kt │ │ │ │ │ ├── PostStatusBottomBar.kt │ │ │ │ │ └── PostStatusPoll.kt │ │ │ │ │ └── usecase │ │ │ │ │ ├── GenerateInitPostStatusUiStateUseCase.kt │ │ │ │ │ └── PublishPostUseCase.kt │ │ │ ├── trending │ │ │ │ ├── TrendingStatusSubViewModel.kt │ │ │ │ ├── TrendingStatusTab.kt │ │ │ │ └── TrendingStatusViewModel.kt │ │ │ └── user │ │ │ │ ├── DetailHeaderContent.kt │ │ │ │ ├── DetailTopBar.kt │ │ │ │ ├── UserDetailContainerViewModel.kt │ │ │ │ ├── UserDetailScreen.kt │ │ │ │ ├── UserDetailUiState.kt │ │ │ │ ├── UserDetailViewModel.kt │ │ │ │ ├── about │ │ │ │ ├── UserAboutContainerViewModel.kt │ │ │ │ ├── UserAboutTab.kt │ │ │ │ ├── UserAboutUiState.kt │ │ │ │ └── UserAboutViewModel.kt │ │ │ │ ├── list │ │ │ │ ├── UserListScreen.kt │ │ │ │ ├── UserListType.kt │ │ │ │ ├── UserListUiState.kt │ │ │ │ └── UserListViewModel.kt │ │ │ │ ├── search │ │ │ │ ├── SearchUserScreen.kt │ │ │ │ ├── SearchUserUiState.kt │ │ │ │ └── SearchUserViewModel.kt │ │ │ │ ├── status │ │ │ │ ├── StatusListScreen.kt │ │ │ │ ├── StatusListScreenRoute.kt │ │ │ │ ├── StatusListType.kt │ │ │ │ └── StatusListViewModel.kt │ │ │ │ ├── tags │ │ │ │ ├── TagListScreen.kt │ │ │ │ ├── TagListScreenRoute.kt │ │ │ │ ├── TagListUiState.kt │ │ │ │ └── TagListViewModel.kt │ │ │ │ └── timeline │ │ │ │ ├── UserTimelineContainerViewModel.kt │ │ │ │ ├── UserTimelineTab.kt │ │ │ │ ├── UserTimelineTabType.kt │ │ │ │ ├── UserTimelineUiState.kt │ │ │ │ └── UserTimelineViewModel.kt │ │ │ ├── source │ │ │ └── UserSourceTransformer.kt │ │ │ ├── uri │ │ │ ├── ActivityPubUriHost.kt │ │ │ ├── ActivityPubUriPath.kt │ │ │ ├── PlatformUriTransformer.kt │ │ │ ├── StatusProviderUriExts.kt │ │ │ └── UserUriTransformer.kt │ │ │ ├── usecase │ │ │ ├── GetInstanceAnnouncementUseCase.kt │ │ │ ├── GetServerTrendTagsUseCase.kt │ │ │ ├── ResolveBaseUrlUseCase.kt │ │ │ ├── UpdateActivityPubUserListUseCase.kt │ │ │ ├── content │ │ │ │ ├── GetUserCreatedListUseCase.kt │ │ │ │ └── ReorderActivityPubTabUseCase.kt │ │ │ ├── emoji │ │ │ │ ├── GetCustomEmojiUseCase.kt │ │ │ │ └── MapCustomEmojiUseCase.kt │ │ │ ├── media │ │ │ │ └── UploadMediaAttachmentUseCase.kt │ │ │ ├── platform │ │ │ │ └── GetInstancePostStatusRulesUseCase.kt │ │ │ ├── source │ │ │ │ └── user │ │ │ │ │ └── SearchUserSourceUseCase.kt │ │ │ └── status │ │ │ │ ├── GetStatusContextUseCase.kt │ │ │ │ ├── GetTimelineStatusUseCase.kt │ │ │ │ ├── GetUserStatusUseCase.kt │ │ │ │ ├── StatusInteractiveUseCase.kt │ │ │ │ └── VotePollUseCase.kt │ │ │ └── utils │ │ │ ├── Base64Utils.kt │ │ │ ├── DeleteTextUtil.kt │ │ │ └── MastodonHelper.kt │ │ └── iosMain │ │ └── kotlin │ │ └── com │ │ └── zhangke │ │ └── fread │ │ └── activitypub │ │ └── app │ │ ├── di │ │ └── ActivityPubComponent.ios.kt │ │ └── internal │ │ └── push │ │ ├── ActivityPubPushManager.ios.kt │ │ └── ActivityPubPushMessageReceiverHelper.ios.kt ├── bluesky │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ │ ├── androidMain │ │ └── kotlin │ │ │ └── com │ │ │ └── zhangke │ │ │ └── fread │ │ │ └── bluesky │ │ │ └── BlueskyPlatformComponent.android.kt │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── zhangke │ │ │ └── fread │ │ │ └── bluesky │ │ │ └── ExampleInstrumentedTest.kt │ │ ├── commonMain │ │ ├── composeResources │ │ │ ├── drawable │ │ │ │ └── bluesky_logo.xml │ │ │ ├── values-zh │ │ │ │ └── strings.xml │ │ │ └── values │ │ │ │ └── strings.xml │ │ └── kotlin │ │ │ └── com │ │ │ └── zhangke │ │ │ └── fread │ │ │ └── bluesky │ │ │ ├── BlueskyAccountManager.kt │ │ │ ├── BlueskyComponent.kt │ │ │ ├── BlueskyNotificationResolver.kt │ │ │ ├── BlueskyPlatformResolver.kt │ │ │ ├── BlueskyProtocol.kt │ │ │ ├── BlueskyProvider.kt │ │ │ ├── BlueskyPublishManager.kt │ │ │ ├── BlueskyScreenProvider.kt │ │ │ ├── BlueskySearchEngine.kt │ │ │ ├── BlueskyStatusResolver.kt │ │ │ ├── BlueskyStatusSourceResolver.kt │ │ │ ├── BskyStartup.kt │ │ │ └── internal │ │ │ ├── account │ │ │ ├── BlueskyLoggedAccount.kt │ │ │ └── BlueskyLoggedAccountManager.kt │ │ │ ├── adapter │ │ │ ├── BlueskyAccountAdapter.kt │ │ │ ├── BlueskyFeedsAdapter.kt │ │ │ ├── BlueskyNotificationAdapter.kt │ │ │ ├── BlueskyProfileAdapter.kt │ │ │ └── BlueskyStatusAdapter.kt │ │ │ ├── client │ │ │ ├── BlueskyClient.kt │ │ │ ├── BlueskyClientManager.kt │ │ │ ├── BlueskyResponseUtils.kt │ │ │ ├── BskyCollections.kt │ │ │ ├── BskyHttpPlugin.kt │ │ │ ├── records.kt │ │ │ └── rkeys.kt │ │ │ ├── composable │ │ │ ├── BlueskyFeedsUi.kt │ │ │ └── DetailTopBar.kt │ │ │ ├── content │ │ │ ├── BlueskyContent.kt │ │ │ └── BlueskyContentManager.kt │ │ │ ├── db │ │ │ ├── BlueskyLoggedAccountDatabase.kt │ │ │ └── converter │ │ │ │ ├── BlueskyLoggedAccountConverter.kt │ │ │ │ └── GeneratorViewConverter.kt │ │ │ ├── model │ │ │ ├── BlueskyFeeds.kt │ │ │ ├── BlueskyProfile.kt │ │ │ ├── BskyPagingFeeds.kt │ │ │ └── CompletedBskyNotification.kt │ │ │ ├── repo │ │ │ ├── BlueskyLoggedAccountRepo.kt │ │ │ └── BlueskyPlatformRepo.kt │ │ │ ├── screen │ │ │ ├── BlueskyRoutes.kt │ │ │ ├── add │ │ │ │ ├── AddBlueskyContentScreen.kt │ │ │ │ ├── AddBlueskyContentUiState.kt │ │ │ │ └── AddBlueskyContentViewModel.kt │ │ │ ├── explorer │ │ │ │ └── ExplorerTab.kt │ │ │ ├── feeds │ │ │ │ ├── detail │ │ │ │ │ ├── FeedsDetailScreen.kt │ │ │ │ │ ├── FeedsDetailUiState.kt │ │ │ │ │ └── FeedsDetailViewModel.kt │ │ │ │ ├── explorer │ │ │ │ │ ├── ExplorerFeedsScreen.kt │ │ │ │ │ ├── ExplorerFeedsUiState.kt │ │ │ │ │ └── ExplorerFeedsViewModel.kt │ │ │ │ ├── following │ │ │ │ │ ├── BskyFollowingFeedsPage.kt │ │ │ │ │ ├── BskyFollowingFeedsUiState.kt │ │ │ │ │ └── BskyFollowingFeedsViewModel.kt │ │ │ │ └── home │ │ │ │ │ ├── HomeFeedsContainerViewModel.kt │ │ │ │ │ ├── HomeFeedsScreen.kt │ │ │ │ │ ├── HomeFeedsTab.kt │ │ │ │ │ └── HomeFeedsViewModel.kt │ │ │ ├── home │ │ │ │ ├── BlueskyHomeContainerViewModel.kt │ │ │ │ ├── BlueskyHomeTab.kt │ │ │ │ ├── BlueskyHomeUiState.kt │ │ │ │ └── BlueskyHomeViewModel.kt │ │ │ ├── publish │ │ │ │ ├── PublishPostEmbed.kt │ │ │ │ ├── PublishPostMediaAttachment.kt │ │ │ │ ├── PublishPostScreen.kt │ │ │ │ ├── PublishPostUiState.kt │ │ │ │ └── PublishPostViewModel.kt │ │ │ └── user │ │ │ │ ├── detail │ │ │ │ ├── BskyUserDetailScreen.kt │ │ │ │ ├── BskyUserDetailUiState.kt │ │ │ │ └── BskyUserDetailViewModel.kt │ │ │ │ ├── edit │ │ │ │ ├── EditProfileScreen.kt │ │ │ │ ├── EditProfileUiState.kt │ │ │ │ └── EditProfileViewModel.kt │ │ │ │ └── list │ │ │ │ ├── UserListItemUiState.kt │ │ │ │ ├── UserListScreen.kt │ │ │ │ ├── UserListType.kt │ │ │ │ └── UserListViewModel.kt │ │ │ ├── tracking │ │ │ └── BskyTrackingElements.kt │ │ │ ├── uri │ │ │ ├── BlueskyUriHost.kt │ │ │ ├── BlueskyUriPath.kt │ │ │ ├── platform │ │ │ │ ├── PlatformUriInsights.kt │ │ │ │ └── PlatformUriTransformer.kt │ │ │ └── user │ │ │ │ ├── UserUriInsights.kt │ │ │ │ └── UserUriTransformer.kt │ │ │ ├── usecase │ │ │ ├── BskyStatusInteractiveUseCase.kt │ │ │ ├── CreateRecordUseCase.kt │ │ │ ├── DeleteRecordUseCase.kt │ │ │ ├── GetAllListsUseCase.kt │ │ │ ├── GetAtIdentifierUseCase.kt │ │ │ ├── GetCompletedNotificationUseCase.kt │ │ │ ├── GetFeedsStatusUseCase.kt │ │ │ ├── GetFollowingFeedsUseCase.kt │ │ │ ├── GetStatusContextUseCase.kt │ │ │ ├── LoginToBskyUseCase.kt │ │ │ ├── PinFeedsUseCase.kt │ │ │ ├── PublishingPostUseCase.kt │ │ │ ├── RefreshSessionUseCase.kt │ │ │ ├── UnpinFeedsUseCase.kt │ │ │ ├── UpdateBlockUseCase.kt │ │ │ ├── UpdateHomeTabUseCase.kt │ │ │ ├── UpdatePinnedFeedsOrderUseCase.kt │ │ │ ├── UpdatePreferencesUseCase.kt │ │ │ ├── UpdateProfileRecordUseCase.kt │ │ │ ├── UpdateRelationshipUseCase.kt │ │ │ └── UploadBlobUseCase.kt │ │ │ └── utils │ │ │ ├── AtResponseUtils.kt │ │ │ └── Tid.kt │ │ ├── iosMain │ │ └── kotlin │ │ │ └── com │ │ │ └── zhangke │ │ │ └── fread │ │ │ └── bluesky │ │ │ └── BlueskyPlatformComponent.ios.kt │ │ └── test │ │ └── java │ │ └── com │ │ └── zhangke │ │ └── fread │ │ └── bluesky │ │ └── ExampleUnitTest.kt └── rss │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ ├── androidMain │ └── kotlin │ │ └── com │ │ └── zhangke │ │ └── fread │ │ └── rss │ │ ├── di │ │ └── RssComponent.android.kt │ │ └── internal │ │ └── utils │ │ └── AvatarUtils.android.kt │ ├── commonMain │ ├── composeResources │ │ ├── values-zh │ │ │ └── strings.xml │ │ └── values │ │ │ └── strings.xml │ └── kotlin │ │ └── com │ │ └── zhangke │ │ └── fread │ │ └── rss │ │ ├── RssAccountManager.kt │ │ ├── RssContentManager.kt │ │ ├── RssNotificationResolver.kt │ │ ├── RssPlatformResolver.kt │ │ ├── RssProtocol.kt │ │ ├── RssPublishManager.kt │ │ ├── RssScreenProvider.kt │ │ ├── RssSearchEngine.kt │ │ ├── RssStatusProvider.kt │ │ ├── RssStatusResolver.kt │ │ ├── RssStatusSourceResolver.kt │ │ ├── di │ │ └── RssComponent.kt │ │ └── internal │ │ ├── adapter │ │ ├── BlogAuthorAdapter.kt │ │ └── RssStatusAdapter.kt │ │ ├── db │ │ ├── RssChannelTable.kt │ │ ├── RssDatabases.kt │ │ └── converter │ │ │ ├── FormalUriConverter.kt │ │ │ └── InstantConverter.kt │ │ ├── model │ │ ├── RssChannelItem.kt │ │ └── RssSource.kt │ │ ├── platform │ │ └── RssPlatformTransformer.kt │ │ ├── repo │ │ ├── RssRepo.kt │ │ └── RssStatusRepo.kt │ │ ├── rss │ │ ├── RssChannel.kt │ │ ├── RssFetcher.kt │ │ ├── RssImage.kt │ │ ├── RssItem.kt │ │ ├── RssParserWrapper.kt │ │ └── adapter │ │ │ ├── RssChannelAdapter.kt │ │ │ ├── RssImageAdapter.kt │ │ │ └── RssItemAdapter.kt │ │ ├── screen │ │ ├── RssRoutes.kt │ │ └── source │ │ │ ├── RssSourceScreen.kt │ │ │ ├── RssSourceUiState.kt │ │ │ └── RssSourceViewModel.kt │ │ ├── source │ │ └── RssSourceTransformer.kt │ │ ├── uri │ │ ├── RssUri.kt │ │ ├── RssUriHost.kt │ │ ├── RssUriInsight.kt │ │ ├── RssUriPath.kt │ │ └── RssUriTransformer.kt │ │ ├── utils │ │ └── AvatarUtils.kt │ │ └── webfinger │ │ └── RssSourceWebFingerTransformer.kt │ └── iosMain │ └── kotlin │ └── com │ └── zhangke │ └── fread │ └── rss │ ├── di │ └── RssComponent.ios.kt │ └── internal │ └── utils │ └── AvatarUtils.ios.kt ├── privacy_cn.md ├── privacy_en.md ├── screenshot └── screenshot.jpg ├── settings.gradle.kts └── thirds ├── halilibo-richtext-material3 ├── build.gradle.kts └── src │ ├── androidMain │ └── AndroidManifest.xml │ └── commonMain │ └── kotlin │ └── com │ └── halilibo │ └── richtext │ └── ui │ └── material3 │ └── RichText.kt └── halilibo-richtext-ui ├── build.gradle.kts └── src ├── androidMain ├── AndroidManifest.xml └── kotlin │ └── com │ └── halilibo │ └── richtext │ └── ui │ ├── CodeBlock.android.kt │ └── util │ └── UUID.android.kt ├── commonMain └── kotlin │ └── com │ └── halilibo │ └── richtext │ └── ui │ ├── BasicRichText.kt │ ├── BlockQuote.kt │ ├── CodeBlock.kt │ ├── FormattedList.kt │ ├── Heading.kt │ ├── HorizontalRule.kt │ ├── InfoPanel.kt │ ├── RichTextLocals.kt │ ├── RichTextScope.kt │ ├── RichTextStyle.kt │ ├── RichTextThemeConfiguration.kt │ ├── RichTextThemeProvider.kt │ ├── SimpleTableLayout.kt │ ├── Table.kt │ ├── string │ ├── InlineContent.kt │ ├── RichTextString.kt │ └── Text.kt │ └── util │ ├── ConditionalTapGestureDetector.kt │ └── UUID.kt └── iosMain └── kotlin └── com └── halilibo └── richtext └── ui ├── CodeBlock.ios.kt └── util └── UUID.ios.kt /.github/ci-gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=2g -Dfile.encoding=UTF-8 2 | org.gradle.daemon=false 3 | org.gradle.parallel=true 4 | org.gradle.workers.max=2 5 | 6 | # kotlin 7 | kotlin.compiler.execution.strategy=in-process 8 | kotlin.native.ignoreDisabledTargets=true 9 | 10 | # other 11 | warningsAsErrors=false -------------------------------------------------------------------------------- /app-hosting/src/androidMain/kotlin/com/zhangke/fread/composable/LoadingPage.android.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.composable 2 | 3 | import androidx.compose.foundation.layout.fillMaxSize 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | import androidx.compose.ui.tooling.preview.Preview 7 | 8 | @Preview 9 | @Composable 10 | private fun PreviewLoadingPage() { 11 | LoadingPage(modifier = Modifier.fillMaxSize()) 12 | } 13 | -------------------------------------------------------------------------------- /app-hosting/src/androidMain/kotlin/com/zhangke/fread/utils/ActivityHelper.android.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.utils 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import com.zhangke.fread.common.di.ActivityScope 6 | import me.tatarka.inject.annotations.Inject 7 | 8 | @ActivityScope 9 | actual class ActivityHelper @Inject constructor( 10 | private val activity: Activity, 11 | ) { 12 | actual fun goHome() { 13 | val intent = Intent(Intent.ACTION_MAIN).apply { 14 | flags = Intent.FLAG_ACTIVITY_NEW_TASK 15 | addCategory(Intent.CATEGORY_HOME) 16 | } 17 | activity.startActivity(intent) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app-hosting/src/commonMain/kotlin/com/zhangke/fread/composable/LoadingPage.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.composable 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.material3.CircularProgressIndicator 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Alignment 8 | import androidx.compose.ui.Modifier 9 | 10 | @Composable 11 | fun LoadingPage(modifier: Modifier = Modifier) { 12 | Box(modifier = modifier.fillMaxSize(), Alignment.Center) { 13 | CircularProgressIndicator() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app-hosting/src/commonMain/kotlin/com/zhangke/fread/di/HostingActivityComponent.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.di 2 | 3 | import com.zhangke.fread.common.CommonActivityComponent 4 | import com.zhangke.fread.utils.ActivityHelper 5 | 6 | interface HostingActivityComponent : CommonActivityComponent { 7 | val activityHelper: ActivityHelper 8 | } -------------------------------------------------------------------------------- /app-hosting/src/commonMain/kotlin/com/zhangke/fread/screen/FreadScreen.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.screen 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.zhangke.fread.common.page.BaseScreen 5 | import com.zhangke.fread.screen.main.MainPage 6 | 7 | class FreadScreen : BaseScreen() { 8 | 9 | @Composable 10 | override fun Content() { 11 | super.Content() 12 | MainPage() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app-hosting/src/commonMain/kotlin/com/zhangke/fread/screen/main/MainPageUiState.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.screen.main 2 | 3 | import com.zhangke.fread.common.update.AppReleaseInfo 4 | 5 | data class MainPageUiState( 6 | val newAppReleaseInfo: AppReleaseInfo?, 7 | ) 8 | -------------------------------------------------------------------------------- /app-hosting/src/commonMain/kotlin/com/zhangke/fread/screen/main/drawer/MainDrawerUiState.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.screen.main.drawer 2 | 3 | import com.zhangke.fread.status.model.FreadContent 4 | 5 | data class MainDrawerUiState( 6 | val contentConfigList: List 7 | ) 8 | -------------------------------------------------------------------------------- /app-hosting/src/commonMain/kotlin/com/zhangke/fread/utils/ActivityHelper.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.utils 2 | 3 | import androidx.compose.runtime.staticCompositionLocalOf 4 | 5 | expect class ActivityHelper { 6 | fun goHome() 7 | } 8 | 9 | internal val LocalActivityHelper = staticCompositionLocalOf { 10 | error("No ActivityHelper provided") 11 | } 12 | -------------------------------------------------------------------------------- /app-hosting/src/iosMain/kotlin/com/zhangke/fread/screen/FreadViewController.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.screen 2 | 3 | import androidx.compose.ui.window.ComposeUIViewController 4 | import me.tatarka.inject.annotations.Inject 5 | import platform.UIKit.UIViewController 6 | 7 | typealias FreadViewController = () -> UIViewController 8 | 9 | @Suppress("FunctionName") 10 | @Inject 11 | internal fun FreadViewController( 12 | iosFreadApp: IosFreadApp, 13 | ): UIViewController = ComposeUIViewController { 14 | iosFreadApp() 15 | } 16 | -------------------------------------------------------------------------------- /app-hosting/src/iosMain/kotlin/com/zhangke/fread/startup/KRouterStartup.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.startup 2 | 3 | import com.zhangke.framework.module.ModuleStartup 4 | import com.zhangke.krouter.KRouter 5 | import me.tatarka.inject.annotations.Inject 6 | 7 | class KRouterStartup @Inject constructor() : ModuleStartup { 8 | override fun onAppCreate() { 9 | @Suppress("UNRESOLVED_REFERENCE") 10 | KRouter.addRouterModule(com.zhangke.krouter.generated.AutoReducingModule()) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app-hosting/src/iosMain/kotlin/com/zhangke/fread/utils/ActivityHelper.ios.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.utils 2 | 3 | import me.tatarka.inject.annotations.Inject 4 | import platform.UIKit.UIViewController 5 | 6 | actual class ActivityHelper @Inject constructor( 7 | private val viewController: Lazy, 8 | ) { 9 | actual fun goHome() { 10 | viewController.value.dismissViewControllerAnimated(false, null) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /debug 3 | /release 4 | /google-services.json 5 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xZhangKe/Fread/4a955bb14196ad5545a8bb2ab691b605bd8b288c/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/com/zhangke/fread/FreadApplication.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread 2 | 3 | /** 4 | * Created by ZhangKe on 2022/11/27. 5 | */ 6 | class FreadApplication : HostingApplication() 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shape_alert_dialog_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xZhangKe/Fread/4a955bb14196ad5545a8bb2ab691b605bd8b288c/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xZhangKe/Fread/4a955bb14196ad5545a8bb2ab691b605bd8b288c/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xZhangKe/Fread/4a955bb14196ad5545a8bb2ab691b605bd8b288c/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xZhangKe/Fread/4a955bb14196ad5545a8bb2ab691b605bd8b288c/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xZhangKe/Fread/4a955bb14196ad5545a8bb2ab691b605bd8b288c/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xZhangKe/Fread/4a955bb14196ad5545a8bb2ab691b605bd8b288c/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xZhangKe/Fread/4a955bb14196ad5545a8bb2ab691b605bd8b288c/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xZhangKe/Fread/4a955bb14196ad5545a8bb2ab691b605bd8b288c/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xZhangKe/Fread/4a955bb14196ad5545a8bb2ab691b605bd8b288c/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xZhangKe/Fread/4a955bb14196ad5545a8bb2ab691b605bd8b288c/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values-zh/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Fread 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #00A5FF 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Fread 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /bizframework/status-provider/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /bizframework/status-provider/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xZhangKe/Fread/4a955bb14196ad5545a8bb2ab691b605bd8b288c/bizframework/status-provider/consumer-rules.pro -------------------------------------------------------------------------------- /bizframework/status-provider/src/androidMain/kotlin/com/zhangke/fread/status/utils/ImplementerFinder.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.status.utils 2 | 3 | import java.util.ServiceLoader 4 | 5 | inline fun findImplementer(): T { 6 | val list = findImplementers() 7 | if (list.size != 1) 8 | throw IllegalStateException("${T::class.qualifiedName} has multiple implementers") 9 | return list.first() 10 | } 11 | 12 | inline fun findImplementers(): List { 13 | return ServiceLoader.load(T::class.java, T::class.java.classLoader) 14 | .iterator() 15 | .asSequence() 16 | .toList() 17 | } 18 | -------------------------------------------------------------------------------- /bizframework/status-provider/src/commonMain/composeResources/values-zh/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 混合内容 · 5 | 条订阅源 6 | 7 | 8 | 9 | 小时 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /bizframework/status-provider/src/commonMain/composeResources/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Mixed Content · 5 | Sources 6 | 7 |  ago 8 | day 9 | hour 10 | min 11 | sec 12 | 13 | -------------------------------------------------------------------------------- /bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/account/AuthenticationFailureException.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.status.account 2 | 3 | class AuthenticationFailureException(override val message: String?) : RuntimeException(message) 4 | 5 | val Throwable.isAuthenticationFailure: Boolean 6 | get() = this is AuthenticationFailureException 7 | -------------------------------------------------------------------------------- /bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/account/LoggedAccount.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.status.account 2 | 3 | import com.zhangke.framework.utils.WebFinger 4 | import com.zhangke.fread.status.model.Emoji 5 | import com.zhangke.fread.status.platform.BlogPlatform 6 | import com.zhangke.fread.status.uri.FormalUri 7 | 8 | interface LoggedAccount { 9 | val uri: FormalUri 10 | val webFinger: WebFinger 11 | val platform: BlogPlatform 12 | val userName: String 13 | val description: String? 14 | val avatar: String? 15 | val emojis: List 16 | val prettyHandle: String 17 | } 18 | -------------------------------------------------------------------------------- /bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/blog/BlogMedia.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.status.blog 2 | 3 | import com.zhangke.framework.utils.PlatformSerializable 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class BlogMedia( 8 | val id: String, 9 | val url: String, 10 | val type: BlogMediaType, 11 | val previewUrl: String?, 12 | val remoteUrl: String?, 13 | val description: String?, 14 | val blurhash: String?, 15 | val meta: BlogMediaMeta?, 16 | ): PlatformSerializable -------------------------------------------------------------------------------- /bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/blog/BlogMediaType.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.status.blog 2 | 3 | enum class BlogMediaType { 4 | 5 | UNKNOWN, 6 | IMAGE, 7 | GIFV, 8 | VIDEO, 9 | AUDIO, 10 | } 11 | -------------------------------------------------------------------------------- /bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/blog/BlogServer.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.status.blog 2 | 3 | data class BlogServer( 4 | val baseUrl: String, 5 | val name: String, 6 | val description: String, 7 | val avatar: String?, 8 | val protocol: String, 9 | ) -------------------------------------------------------------------------------- /bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/blog/PostingApplication.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.status.blog 2 | 3 | import com.zhangke.framework.utils.PlatformSerializable 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class PostingApplication( 8 | val name: String, 9 | val website: String?, 10 | ) : PlatformSerializable 11 | -------------------------------------------------------------------------------- /bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/model/ContentType.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.status.model 2 | 3 | enum class ContentType { 4 | 5 | MIXED, 6 | ACTIVITY_PUB, 7 | BLUESKY, 8 | 9 | } 10 | -------------------------------------------------------------------------------- /bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/model/Emoji.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.status.model 2 | 3 | import com.zhangke.framework.utils.PlatformSerializable 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class Emoji( 8 | val shortcode: String, 9 | val url: String, 10 | val staticUrl: String, 11 | ): PlatformSerializable 12 | -------------------------------------------------------------------------------- /bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/model/FacetFeatureUnion.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.status.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class Facet( 7 | val byteStart: Long, 8 | val byteEnd: Long, 9 | val features: List, 10 | ) 11 | 12 | @Serializable 13 | sealed interface FacetFeatureUnion { 14 | 15 | @Serializable 16 | data class Mention(val did: String) : FacetFeatureUnion 17 | 18 | @Serializable 19 | data class Tag(val tag: String) : FacetFeatureUnion 20 | 21 | @Serializable 22 | data class Link(val uri: String) : FacetFeatureUnion 23 | } 24 | -------------------------------------------------------------------------------- /bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/model/FreadContent.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.status.model 2 | 3 | import androidx.compose.runtime.Composable 4 | 5 | interface FreadContent { 6 | 7 | val id: String 8 | 9 | val order: Int 10 | 11 | val name: String 12 | 13 | fun newOrder(newOrder: Int): FreadContent 14 | 15 | @Composable 16 | fun Subtitle() 17 | } 18 | -------------------------------------------------------------------------------- /bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/model/Hashtag.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.status.model 2 | 3 | import com.zhangke.framework.composable.TextString 4 | 5 | data class Hashtag( 6 | val name: String, 7 | val url: String, 8 | val description: TextString, 9 | val history: History, 10 | val following: Boolean, 11 | val protocol: StatusProviderProtocol, 12 | ) { 13 | 14 | data class History( 15 | val history: List, 16 | val min: Float?, 17 | val max: Float?, 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/model/HashtagInStatus.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.status.model 2 | 3 | import com.zhangke.framework.utils.PlatformSerializable 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class HashtagInStatus( 8 | val name: String, 9 | val url: String, 10 | val protocol: StatusProviderProtocol, 11 | ): PlatformSerializable 12 | -------------------------------------------------------------------------------- /bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/model/Mention.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.status.model 2 | 3 | import com.zhangke.framework.utils.PlatformSerializable 4 | import com.zhangke.framework.utils.WebFinger 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class Mention( 9 | val id: String, 10 | val username: String, 11 | val url: String, 12 | val webFinger: WebFinger, 13 | val protocol: StatusProviderProtocol, 14 | ): PlatformSerializable 15 | -------------------------------------------------------------------------------- /bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/model/PagedData.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.status.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class PagedData( 7 | val list: List, 8 | val cursor: String?, 9 | ) 10 | -------------------------------------------------------------------------------- /bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/model/PublishBlogRules.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.status.model 2 | 3 | data class PublishBlogRules( 4 | val maxCharacters: Int, 5 | val maxMediaCount: Int, 6 | val mediaAltMaxCharacters: Int, 7 | val maxPollOptions: Int, 8 | val supportSpoiler: Boolean, 9 | val supportPoll: Boolean, 10 | val maxLanguageCount: Int, 11 | ) 12 | -------------------------------------------------------------------------------- /bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/model/StatusActionType.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.status.model 2 | 3 | enum class StatusActionType { 4 | 5 | LIKE, 6 | FORWARD, 7 | BOOKMARK, 8 | REPLY, 9 | DELETE, 10 | SHARE, 11 | PIN, 12 | EDIT, 13 | QUOTE, 14 | } 15 | -------------------------------------------------------------------------------- /bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/model/StatusList.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.status.model 2 | 3 | data class StatusList( 4 | val name: String, 5 | val cid: String, 6 | val uri: String, 7 | ) -------------------------------------------------------------------------------- /bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/model/StatusVisibility.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.status.model 2 | 3 | import com.zhangke.framework.utils.PlatformSerializable 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | enum class StatusVisibility : PlatformSerializable { 8 | PUBLIC, 9 | UNLISTED, 10 | PRIVATE, 11 | DIRECT, 12 | } 13 | -------------------------------------------------------------------------------- /bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/platform/PlatformSnapshot.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.status.platform 2 | 3 | import com.zhangke.fread.status.model.StatusProviderProtocol 4 | 5 | data class PlatformSnapshot( 6 | val uri: String? = null, 7 | val name: String? = null, 8 | val domain: String, 9 | val description: String, 10 | val thumbnail: String, 11 | val protocol: StatusProviderProtocol, 12 | val priority: Int = 0, 13 | ) 14 | -------------------------------------------------------------------------------- /bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/richtext/OnLinkTargetClick.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.status.richtext 2 | 3 | import com.zhangke.fread.status.richtext.model.RichLinkTarget 4 | 5 | typealias OnLinkTargetClick = (RichLinkTarget) -> Unit -------------------------------------------------------------------------------- /bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/richtext/model/RichLinkTarget.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.status.richtext.model 2 | 3 | import com.zhangke.fread.status.model.HashtagInStatus 4 | import com.zhangke.fread.status.model.Mention 5 | 6 | sealed interface RichLinkTarget { 7 | 8 | data class UrlTarget(val url: String) : RichLinkTarget 9 | 10 | data class MentionTarget(val mention: Mention) : RichLinkTarget 11 | 12 | data class MentionDidTarget(val did: String) : RichLinkTarget 13 | 14 | data class HashtagTarget(val hashtag: HashtagInStatus) : RichLinkTarget 15 | 16 | data class MaybeHashtagTarget(val hashtag: String) : RichLinkTarget 17 | } -------------------------------------------------------------------------------- /bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/source/StatusSource.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.status.source 2 | 3 | import com.zhangke.framework.utils.Parcelize 4 | import com.zhangke.framework.utils.PlatformParcelable 5 | import com.zhangke.framework.utils.PlatformSerializable 6 | import com.zhangke.fread.status.model.StatusProviderProtocol 7 | import com.zhangke.fread.status.uri.FormalUri 8 | 9 | @Parcelize 10 | data class StatusSource( 11 | val uri: FormalUri, 12 | val name: String, 13 | val handle: String, 14 | val description: String, 15 | val thumbnail: String?, 16 | val protocol: StatusProviderProtocol, 17 | ): PlatformParcelable, PlatformSerializable 18 | -------------------------------------------------------------------------------- /bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/status/model/StatusContext.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.status.status.model 2 | 3 | import com.zhangke.fread.status.model.StatusUiState 4 | 5 | data class StatusContext( 6 | val ancestors: List, 7 | val status: StatusUiState?, 8 | val descendants: List, 9 | ) 10 | 11 | data class DescendantStatus( 12 | val status: StatusUiState, 13 | val descendantStatus: DescendantStatus?, 14 | ) 15 | -------------------------------------------------------------------------------- /bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/utils/ResultKtx.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.status.utils 2 | 3 | fun List>>.collect(): Result> { 4 | if (this.isEmpty()) return Result.success(emptyList()) 5 | if (isNotEmpty() && firstOrNull { it.isSuccess } == null) { 6 | return first() 7 | } 8 | return mapNotNull { 9 | it.getOrNull() 10 | }.reduce { list1, list2 -> 11 | mutableListOf().apply { 12 | addAll(list1) 13 | addAll(list2) 14 | } 15 | }.let { Result.success(it) } 16 | } 17 | -------------------------------------------------------------------------------- /build-logic/convention/src/main/kotlin/Project.kt: -------------------------------------------------------------------------------- 1 | import org.gradle.kotlin.dsl.DependencyHandlerScope 2 | import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension 3 | 4 | fun DependencyHandlerScope.kspAll(dependencyNotation: Any) { 5 | add("kspAndroid", dependencyNotation) 6 | add("kspIosSimulatorArm64", dependencyNotation) 7 | add("kspIosX64", dependencyNotation) 8 | add("kspIosArm64", dependencyNotation) 9 | } 10 | 11 | fun KotlinMultiplatformExtension.configureCommonMainKsp() { 12 | sourceSets.named("commonMain").configure { 13 | kotlin.srcDir("build/generated/ksp/metadata/commonMain/kotlin") 14 | } 15 | } -------------------------------------------------------------------------------- /build-logic/convention/src/main/kotlin/ProjectFeatureKmpConventionPlugin.kt: -------------------------------------------------------------------------------- 1 | import org.gradle.api.Plugin 2 | import org.gradle.api.Project 3 | 4 | class ProjectFeatureKmpConventionPlugin: Plugin { 5 | 6 | override fun apply(target: Project) { 7 | with(target) { 8 | with(pluginManager) { 9 | apply("fread.project.framework.kmp") 10 | } 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /build-logic/gradle.properties: -------------------------------------------------------------------------------- 1 | # Gradle properties are not passed to included builds https://github.com/gradle/gradle/issues/2534 2 | org.gradle.parallel=true 3 | org.gradle.caching=true 4 | org.gradle.configureondemand=true 5 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application) apply false 3 | alias(libs.plugins.android.library) apply false 4 | alias(libs.plugins.kotlin.android) apply false 5 | alias(libs.plugins.kotlin.jvm) apply false 6 | alias(libs.plugins.kotlin.multiplatform) apply false 7 | alias(libs.plugins.ksp) apply false 8 | alias(libs.plugins.kotlinx.serialization) apply false 9 | alias(libs.plugins.jetbrains.compose) apply false 10 | alias(libs.plugins.jetbrains.compose.compiler) apply false 11 | alias(libs.plugins.google.service) apply false 12 | alias(libs.plugins.firebase.crashlytics) apply false 13 | } 14 | -------------------------------------------------------------------------------- /commonbiz/analytics/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /commonbiz/analytics/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xZhangKe/Fread/4a955bb14196ad5545a8bb2ab691b605bd8b288c/commonbiz/analytics/consumer-rules.pro -------------------------------------------------------------------------------- /commonbiz/analytics/src/commonMain/kotlin/com/zhangke/fread/analytics/AnalyticsScreenHook.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.analytics 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.zhangke.fread.common.page.BaseScreen 5 | import com.zhangke.fread.common.page.BaseScreenHook 6 | import com.zhangke.krouter.annotation.Service 7 | 8 | @Service(BaseScreenHook::class) 9 | class AnalyticsScreenHook : BaseScreenHook { 10 | 11 | @Composable 12 | override fun HookContent(screen: BaseScreen) { 13 | with(screen) { 14 | TrackingScreenEvent() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /commonbiz/analytics/src/commonMain/kotlin/com/zhangke/fread/analytics/AnalyticsTabHook.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.analytics 2 | 3 | import androidx.compose.runtime.Composable 4 | import cafe.adriel.voyager.core.screen.Screen 5 | import com.zhangke.fread.common.page.BasePagerTab 6 | import com.zhangke.fread.common.page.BasePagerTabHook 7 | import com.zhangke.krouter.annotation.Service 8 | 9 | @Service(BasePagerTabHook::class) 10 | class AnalyticsTabHook : BasePagerTabHook { 11 | 12 | @Composable 13 | override fun HookContent(screen: Screen, tab: BasePagerTab) { 14 | with(tab) { 15 | TrackingTabEvent() 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /commonbiz/analytics/src/commonMain/kotlin/com/zhangke/fread/analytics/EventNames.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.analytics 2 | 3 | object EventNames { 4 | 5 | const val PAGE_SHOW = "page_show" 6 | const val CLICK = "click" 7 | const val INFO = "info" 8 | } 9 | 10 | object EventParamsName { 11 | 12 | const val PAGE_NAME = "page_name" 13 | const val ELEMENT = "element" 14 | } 15 | -------------------------------------------------------------------------------- /commonbiz/analytics/src/commonMain/kotlin/com/zhangke/fread/analytics/TrackingEventDataBuilder.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.analytics 2 | 3 | 4 | class TrackingEventDataBuilder : MutableMap by mutableMapOf() { 5 | 6 | fun putIfNotNull(key: String, value: String?) { 7 | value?.let { put(key, it) } 8 | } 9 | 10 | fun build(): Map = toMap() 11 | } 12 | -------------------------------------------------------------------------------- /commonbiz/common/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /commonbiz/common/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /commonbiz/common/src/androidMain/kotlin/com/zhangke/fread/common/CommonActivityComponent.android.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common 2 | 3 | import com.zhangke.fread.common.browser.ActivityBrowserLauncher 4 | import com.zhangke.fread.common.browser.AndroidActivityBrowserLauncher 5 | import me.tatarka.inject.annotations.Provides 6 | 7 | actual interface CommonActivityPlatformComponent { 8 | @Provides 9 | fun AndroidActivityBrowserLauncher.binds(): ActivityBrowserLauncher = this 10 | } 11 | -------------------------------------------------------------------------------- /commonbiz/common/src/androidMain/kotlin/com/zhangke/fread/common/di/ApplicationContext.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.di 2 | 3 | typealias ApplicationContext = android.content.Context 4 | -------------------------------------------------------------------------------- /commonbiz/common/src/androidMain/kotlin/com/zhangke/fread/common/ext/InstantExt.android.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.ext 2 | 3 | import kotlinx.datetime.Instant 4 | import java.util.Date 5 | 6 | fun Instant.toJavaDate(): Date { 7 | return Date(toEpochMilliseconds()) 8 | } 9 | -------------------------------------------------------------------------------- /commonbiz/common/src/androidMain/kotlin/com/zhangke/fread/common/page/BasePagerTabHookManager.android.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.page 2 | 3 | import com.zhangke.fread.status.utils.findImplementers 4 | 5 | internal actual fun findBasePagerTabImplementers(): List { 6 | return findImplementers() 7 | } -------------------------------------------------------------------------------- /commonbiz/common/src/androidMain/kotlin/com/zhangke/fread/common/startup/LanguageModuleStartup.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.startup 2 | 3 | import com.zhangke.framework.module.ModuleStartup 4 | import com.zhangke.fread.common.language.LanguageHelper 5 | import me.tatarka.inject.annotations.Inject 6 | 7 | class LanguageModuleStartup @Inject constructor( 8 | private val languageHelper: LanguageHelper, 9 | ): ModuleStartup { 10 | 11 | override fun onAppCreate() { 12 | languageHelper.init() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /commonbiz/common/src/androidMain/kotlin/com/zhangke/fread/common/utils/ActivityResultUtils.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.utils 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | 6 | typealias ActivityResultCallback = (resultCode: Int, Intent?) -> Unit 7 | 8 | fun Activity.registerActivityResultCallback( 9 | requestCode: Int, 10 | callback: ActivityResultCallback, 11 | ) { 12 | if (this is CallbackableActivity) { 13 | this.registerCallback(requestCode, callback) 14 | } 15 | } 16 | 17 | interface CallbackableActivity { 18 | 19 | fun registerCallback(requestCode: Int, callback: ActivityResultCallback) 20 | } 21 | -------------------------------------------------------------------------------- /commonbiz/common/src/androidMain/kotlin/com/zhangke/fread/common/utils/DateTypeConverter.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.utils 2 | 3 | import androidx.room.TypeConverter 4 | import java.util.Date 5 | 6 | class DateTypeConverter { 7 | 8 | @TypeConverter 9 | fun fromLong(time: Long): Date { 10 | return Date(time) 11 | } 12 | 13 | @TypeConverter 14 | fun toLong(date: Date): Long { 15 | return date.time 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /commonbiz/common/src/androidMain/kotlin/com/zhangke/fread/common/utils/RandomIdGenerator.android.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.utils 2 | 3 | import java.util.UUID 4 | 5 | actual class RandomIdGenerator { 6 | 7 | actual fun generateId(): String { 8 | return UUID.randomUUID().toString() 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /commonbiz/common/src/androidMain/kotlin/com/zhangke/fread/common/utils/StorageHelper.android.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.utils 2 | 3 | import com.zhangke.fread.common.di.ApplicationContext 4 | import com.zhangke.fread.common.di.ApplicationScope 5 | import me.tatarka.inject.annotations.Inject 6 | import okio.Path 7 | import okio.Path.Companion.toOkioPath 8 | 9 | @ApplicationScope 10 | actual class StorageHelper @Inject constructor( 11 | private val applicationContext: ApplicationContext, 12 | ) { 13 | actual val cacheDir: Path 14 | get() = applicationContext.cacheDir.toOkioPath() 15 | } -------------------------------------------------------------------------------- /commonbiz/common/src/androidMain/kotlin/com/zhangke/fread/common/utils/ToastHelper.android.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.utils 2 | 3 | import android.app.Activity 4 | import android.view.Gravity 5 | import android.widget.Toast 6 | import com.zhangke.fread.common.di.ActivityScope 7 | import me.tatarka.inject.annotations.Inject 8 | 9 | @ActivityScope 10 | actual class ToastHelper @Inject constructor( 11 | private val activity: Activity, 12 | ) { 13 | actual fun showToast(content: String) { 14 | val toast = Toast.makeText( 15 | activity, 16 | content, 17 | Toast.LENGTH_SHORT, 18 | ) 19 | toast.setGravity(Gravity.CENTER, 0, 0) 20 | toast.show() 21 | } 22 | } -------------------------------------------------------------------------------- /commonbiz/common/src/androidMain/res/anim/fade_in.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /commonbiz/common/src/androidMain/res/anim/fade_out.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /commonbiz/common/src/androidMain/res/drawable-hdpi/ic_fread_logo_small_circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xZhangKe/Fread/4a955bb14196ad5545a8bb2ab691b605bd8b288c/commonbiz/common/src/androidMain/res/drawable-hdpi/ic_fread_logo_small_circle.png -------------------------------------------------------------------------------- /commonbiz/common/src/androidMain/res/drawable-mdpi/ic_fread_logo_small_circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xZhangKe/Fread/4a955bb14196ad5545a8bb2ab691b605bd8b288c/commonbiz/common/src/androidMain/res/drawable-mdpi/ic_fread_logo_small_circle.png -------------------------------------------------------------------------------- /commonbiz/common/src/androidMain/res/drawable-xhdpi/ic_fread_logo_small_circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xZhangKe/Fread/4a955bb14196ad5545a8bb2ab691b605bd8b288c/commonbiz/common/src/androidMain/res/drawable-xhdpi/ic_fread_logo_small_circle.png -------------------------------------------------------------------------------- /commonbiz/common/src/androidMain/res/drawable-xxhdpi/ic_fread_logo_small_circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xZhangKe/Fread/4a955bb14196ad5545a8bb2ab691b605bd8b288c/commonbiz/common/src/androidMain/res/drawable-xxhdpi/ic_fread_logo_small_circle.png -------------------------------------------------------------------------------- /commonbiz/common/src/androidMain/res/drawable-xxxhdpi/ic_fread_logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xZhangKe/Fread/4a955bb14196ad5545a8bb2ab691b605bd8b288c/commonbiz/common/src/androidMain/res/drawable-xxxhdpi/ic_fread_logo.webp -------------------------------------------------------------------------------- /commonbiz/common/src/androidMain/res/drawable-xxxhdpi/ic_fread_logo_small_circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xZhangKe/Fread/4a955bb14196ad5545a8bb2ab691b605bd8b288c/commonbiz/common/src/androidMain/res/drawable-xxxhdpi/ic_fread_logo_small_circle.png -------------------------------------------------------------------------------- /commonbiz/common/src/androidMain/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FF00A5FF 4 | 5 | -------------------------------------------------------------------------------- /commonbiz/common/src/androidMain/res/values/theme.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/composeResources/drawable-xxxhdpi/ic_fread_logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xZhangKe/Fread/4a955bb14196ad5545a8bb2ab691b605bd8b288c/commonbiz/common/src/commonMain/composeResources/drawable-xxxhdpi/ic_fread_logo.webp -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/composeResources/drawable-xxxhdpi/illustration_cards.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xZhangKe/Fread/4a955bb14196ad5545a8bb2ab691b605bd8b288c/commonbiz/common/src/commonMain/composeResources/drawable-xxxhdpi/illustration_cards.png -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/composeResources/drawable-xxxhdpi/illustration_celebrate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xZhangKe/Fread/4a955bb14196ad5545a8bb2ab691b605bd8b288c/commonbiz/common/src/commonMain/composeResources/drawable-xxxhdpi/illustration_celebrate.png -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/composeResources/drawable-xxxhdpi/illustration_explorer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xZhangKe/Fread/4a955bb14196ad5545a8bb2ab691b605bd8b288c/commonbiz/common/src/commonMain/composeResources/drawable-xxxhdpi/illustration_explorer.png -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/composeResources/drawable-xxxhdpi/illustration_inspiration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xZhangKe/Fread/4a955bb14196ad5545a8bb2ab691b605bd8b288c/commonbiz/common/src/commonMain/composeResources/drawable-xxxhdpi/illustration_inspiration.png -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/composeResources/drawable-xxxhdpi/illustration_message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xZhangKe/Fread/4a955bb14196ad5545a8bb2ab691b605bd8b288c/commonbiz/common/src/commonMain/composeResources/drawable-xxxhdpi/illustration_message.png -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/CommonActivityComponent.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common 2 | 3 | import com.zhangke.fread.common.daynight.ActivityDayNightHelper 4 | 5 | expect interface CommonActivityPlatformComponent 6 | 7 | interface CommonActivityComponent : CommonActivityPlatformComponent { 8 | val activityDayNightHelper: ActivityDayNightHelper 9 | } -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/action/ComposableActions.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.action 2 | 3 | import androidx.compose.runtime.staticCompositionLocalOf 4 | import kotlinx.coroutines.flow.MutableSharedFlow 5 | import kotlinx.coroutines.flow.SharedFlow 6 | 7 | val LocalComposableActions = staticCompositionLocalOf { ComposableActions } 8 | 9 | object ComposableActions { 10 | 11 | private val _actionFlow = MutableSharedFlow(replay = 1) 12 | val actionFlow: SharedFlow get() = _actionFlow 13 | 14 | suspend fun post(uri: String) { 15 | _actionFlow.emit(uri) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/action/RouteAction.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.action 2 | 3 | interface RouteAction { 4 | 5 | fun execute() 6 | } 7 | -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/action/RouteActions.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.action 2 | 3 | object RouteActions { 4 | 5 | const val ACTION_KEY = "route_action" 6 | 7 | const val BASE_URI = "fread://fread.xyz/action" 8 | 9 | } 10 | 11 | object OpenNotificationPageAction { 12 | 13 | const val URI = "${RouteActions.BASE_URI}/open_notification_page" 14 | 15 | fun buildOpenNotificationPageRoute(): String { 16 | return buildString { 17 | append(URI) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/browser/BrowserInterceptor.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.browser 2 | 3 | import com.zhangke.fread.status.model.IdentityRole 4 | 5 | interface BrowserInterceptor { 6 | 7 | suspend fun intercept(role: IdentityRole, url: String): Boolean 8 | } 9 | -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/browser/OAuthHandler.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.browser 2 | 3 | expect class OAuthHandler { 4 | suspend fun startOAuth(url: String): String 5 | } 6 | -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/bubble/Bubble.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.bubble 2 | 3 | import androidx.compose.foundation.layout.ColumnScope 4 | import androidx.compose.runtime.Composable 5 | 6 | interface Bubble { 7 | 8 | @Composable 9 | fun ColumnScope.Content() 10 | } 11 | -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/config/StatusConfig.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.config 2 | 3 | data class StatusConfig( 4 | val alwaysShowSensitiveContent: Boolean, 5 | val contentSize: StatusContentSize, 6 | ) { 7 | 8 | companion object { 9 | 10 | fun default( 11 | alwaysShowSensitiveContent: Boolean = false, 12 | contentSize: StatusContentSize = StatusContentSize.default(), 13 | ) = StatusConfig( 14 | alwaysShowSensitiveContent = alwaysShowSensitiveContent, 15 | contentSize = contentSize, 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/config/StatusContentSize.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.config 2 | 3 | enum class StatusContentSize { 4 | 5 | SMALL, 6 | MEDIUM, 7 | LARGE; 8 | 9 | companion object { 10 | 11 | fun default(): StatusContentSize = MEDIUM 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/db/converts/ContentTypeConverter.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.db.converts 2 | 3 | import androidx.room.TypeConverter 4 | import com.zhangke.fread.status.model.ContentType 5 | 6 | class ContentTypeConverter { 7 | 8 | @TypeConverter 9 | fun fromString(text: String): ContentType { 10 | return ContentType.valueOf(text) 11 | } 12 | 13 | @TypeConverter 14 | fun toString(type: ContentType): String { 15 | return type.name 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/db/converts/FormalBaseUrlConverter.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.db.converts 2 | 3 | import androidx.room.TypeConverter 4 | import com.zhangke.framework.network.FormalBaseUrl 5 | 6 | class FormalBaseUrlConverter { 7 | 8 | @TypeConverter 9 | fun fromString(text: String?): FormalBaseUrl? { 10 | text ?: return null 11 | return FormalBaseUrl.parse(text)!! 12 | } 13 | 14 | @TypeConverter 15 | fun toString(baseUrl: FormalBaseUrl?): String? { 16 | baseUrl ?: return null 17 | return baseUrl.toString() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/db/converts/FormalUriConverter.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.db.converts 2 | 3 | import androidx.room.TypeConverter 4 | import com.zhangke.fread.status.uri.FormalUri 5 | 6 | class FormalUriConverter { 7 | 8 | @TypeConverter 9 | fun convertToString(uri: FormalUri): String { 10 | return uri.toString() 11 | } 12 | 13 | @TypeConverter 14 | fun convertToUri(uri: String): FormalUri { 15 | return FormalUri.from(uri)!! 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/db/converts/FreadContentConverter.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.db.converts 2 | 3 | import androidx.room.TypeConverter 4 | import com.zhangke.framework.architect.json.fromJson 5 | import com.zhangke.framework.architect.json.globalJson 6 | import com.zhangke.fread.status.model.FreadContent 7 | import kotlinx.serialization.encodeToString 8 | 9 | class FreadContentConverter { 10 | 11 | @TypeConverter 12 | fun fromJsonText(text: String): FreadContent { 13 | return globalJson.fromJson(text) 14 | } 15 | 16 | @TypeConverter 17 | fun toJsonText(content: FreadContent): String { 18 | return globalJson.encodeToString(content) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/db/converts/IdentityRoleConverter.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.db.converts 2 | 3 | import androidx.room.TypeConverter 4 | import com.zhangke.fread.status.model.IdentityRole 5 | import kotlinx.serialization.json.Json 6 | 7 | class IdentityRoleConverter { 8 | 9 | @TypeConverter 10 | fun fromString(string: String): IdentityRole { 11 | return Json.decodeFromString(IdentityRole.serializer(), string) 12 | } 13 | 14 | @TypeConverter 15 | fun toString(role: IdentityRole): String { 16 | return Json.encodeToString(IdentityRole.serializer(), role) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/db/converts/StatusUiStateConverter.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.db.converts 2 | 3 | import androidx.room.TypeConverter 4 | import com.zhangke.fread.status.model.StatusUiState 5 | import kotlinx.serialization.json.Json 6 | 7 | class StatusUiStateConverter { 8 | 9 | @TypeConverter 10 | fun convertToText(status: StatusUiState): String { 11 | return Json.encodeToString(StatusUiState.serializer(), status) 12 | } 13 | 14 | @TypeConverter 15 | fun convertToStatus(text: String): StatusUiState { 16 | return Json.decodeFromString(StatusUiState.serializer(), text) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/di/ApplicationCoroutineScope.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.di 2 | 3 | typealias ApplicationCoroutineScope = kotlinx.coroutines.CoroutineScope 4 | -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/di/Scopes.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.di 2 | 3 | import me.tatarka.inject.annotations.Scope 4 | 5 | @Scope 6 | annotation class ApplicationScope 7 | 8 | @Scope 9 | annotation class ActivityScope 10 | -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/di/ViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.di 2 | 3 | import androidx.lifecycle.ViewModel 4 | import kotlin.reflect.KClass 5 | 6 | typealias ViewModelKey = KClass 7 | 8 | typealias ViewModelCreator = () -> ViewModel 9 | 10 | interface ViewModelFactory 11 | -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/feeds/model/RefreshResult.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.feeds.model 2 | 3 | import com.zhangke.fread.status.model.StatusUiState 4 | 5 | data class RefreshResult( 6 | val newStatus: List, 7 | val deletedStatus: List, 8 | val useOldData: Boolean = true, 9 | ) { 10 | 11 | companion object { 12 | val EMPTY = RefreshResult(emptyList(), emptyList()) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/language/LanguageHelper.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.language 2 | 3 | import androidx.compose.runtime.staticCompositionLocalOf 4 | 5 | internal const val LANGUAGE_SETTING = "app_language_setting" 6 | 7 | enum class LanguageSettingType(val value: Int) { 8 | CN(1), 9 | EN(2), 10 | SYSTEM(3), 11 | ; 12 | } 13 | 14 | expect class ActivityLanguageHelper { 15 | 16 | val currentType: LanguageSettingType 17 | 18 | fun setLanguage(type: LanguageSettingType) 19 | } 20 | 21 | val LocalActivityLanguageHelper = staticCompositionLocalOf { error("No ActivityLanguageHelper provided") } 22 | -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/page/BasePagerTab.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.page 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.input.nestedscroll.NestedScrollConnection 5 | import cafe.adriel.voyager.core.screen.Screen 6 | import com.zhangke.framework.composable.PagerTab 7 | 8 | abstract class BasePagerTab: PagerTab { 9 | 10 | @Composable 11 | override fun TabContent(screen: Screen, nestedScrollConnection: NestedScrollConnection?) { 12 | BasePagerTabHookManager.hookList.forEach { 13 | it.HookContent(screen, this@BasePagerTab) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/page/BasePagerTabHookManager.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.page 2 | 3 | import androidx.compose.runtime.Composable 4 | import cafe.adriel.voyager.core.screen.Screen 5 | 6 | object BasePagerTabHookManager { 7 | 8 | private val _hookList = mutableListOf() 9 | val hookList: List get() = _hookList 10 | 11 | init { 12 | _hookList.addAll(findBasePagerTabImplementers()) 13 | } 14 | } 15 | 16 | interface BasePagerTabHook { 17 | 18 | @Composable 19 | fun HookContent(screen: Screen, tab: BasePagerTab) 20 | } 21 | 22 | internal expect fun findBasePagerTabImplementers(): List -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/page/BaseScreen.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.page 2 | 3 | import androidx.compose.runtime.Composable 4 | import cafe.adriel.voyager.core.screen.Screen 5 | 6 | open class BaseScreen : Screen { 7 | 8 | @Composable 9 | override fun Content() { 10 | BaseScreenHookManager.hookList.forEach { 11 | it.HookContent(this) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/page/BaseScreenHookManager.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.page 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.zhangke.krouter.KRouter 5 | 6 | object BaseScreenHookManager { 7 | 8 | private val _hookList = mutableListOf() 9 | val hookList: List get() = _hookList 10 | 11 | init { 12 | _hookList.addAll(KRouter.getServices()) 13 | } 14 | } 15 | 16 | interface BaseScreenHook { 17 | 18 | @Composable 19 | fun HookContent(screen: BaseScreen) 20 | } 21 | -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/publish/PublishPostManager.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.publish 2 | 3 | import com.zhangke.fread.common.di.ApplicationScope 4 | import me.tatarka.inject.annotations.Inject 5 | 6 | @ApplicationScope 7 | class PublishPostManager @Inject constructor() { 8 | 9 | fun publish(){ 10 | 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/review/DefaultAppStoreReviewer.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.review 2 | 3 | interface DefaultAppStoreReviewer { 4 | 5 | fun showAppStoreReviewPopup( 6 | onReviewSuccess: () -> Unit, 7 | onReviewCancel: () -> Unit, 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/startup/FeedsRepoModuleStartup.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.startup 2 | 3 | import com.zhangke.framework.module.ModuleStartup 4 | import me.tatarka.inject.annotations.Inject 5 | 6 | class FeedsRepoModuleStartup @Inject constructor( 7 | ) : ModuleStartup { 8 | 9 | override fun onAppCreate() { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/startup/StartupManager.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.startup 2 | 3 | import com.zhangke.framework.module.ModuleStartup 4 | import com.zhangke.fread.common.di.ApplicationScope 5 | import me.tatarka.inject.annotations.Inject 6 | 7 | @ApplicationScope 8 | class StartupManager @Inject constructor( 9 | private val startupList: Set, 10 | ) { 11 | fun initialize() { 12 | startupList.forEach { 13 | it.onAppCreate() 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/status/StatusConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.status 2 | 3 | import kotlin.time.Duration 4 | import kotlin.time.Duration.Companion.minutes 5 | 6 | data class StatusConfiguration( 7 | val loadFromServerLimit: Int, 8 | val loadFromLocalLimit: Int, 9 | val loadFromLocalRedundancies: Int, 10 | val autoFetchNewerFeedsInterval: Duration, 11 | ) 12 | 13 | object StatusConfigurationDefault { 14 | 15 | val config = StatusConfiguration( 16 | loadFromServerLimit = 60, 17 | loadFromLocalLimit = 100, 18 | loadFromLocalRedundancies = 3, 19 | autoFetchNewerFeedsInterval = 2.minutes, 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/status/StatusIdGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.status 2 | 3 | import com.zhangke.fread.status.status.model.Status 4 | import com.zhangke.fread.status.uri.FormalUri 5 | import me.tatarka.inject.annotations.Inject 6 | 7 | class StatusIdGenerator @Inject constructor() { 8 | 9 | fun generate(sourceUri: FormalUri, status: Status): String { 10 | return "${sourceUri.host}_${sourceUri.path}_${status.id}" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/update/AppPlatformUpdater.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.update 2 | 3 | expect class AppPlatformUpdater() { 4 | 5 | val platformName: String 6 | 7 | val signingForFDroid: Boolean 8 | 9 | fun getAppVersionCode(): Long 10 | 11 | fun triggerUpdate(releaseInfo: AppReleaseInfo) 12 | } 13 | -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/update/AppReleaseInfo.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.update 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class AppReleaseInfo( 7 | val versionCode: Long, 8 | val versionName: String, 9 | val releaseNote: String, 10 | val downloadUrl: String, 11 | ) 12 | -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/usecase/GetDefaultBaseUrlUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.usecase 2 | 3 | import com.zhangke.framework.network.FormalBaseUrl 4 | import me.tatarka.inject.annotations.Inject 5 | 6 | class GetDefaultBaseUrlUseCase @Inject constructor() { 7 | 8 | companion object { 9 | 10 | private val defaultBaseUrl = FormalBaseUrl.parse("https://mastodon.online")!! 11 | } 12 | 13 | operator fun invoke(): FormalBaseUrl { 14 | return defaultBaseUrl 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/utils/GlobalScreenNavigation.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.utils 2 | 3 | import cafe.adriel.voyager.core.screen.Screen 4 | import kotlinx.coroutines.flow.MutableSharedFlow 5 | import kotlinx.coroutines.flow.SharedFlow 6 | import kotlinx.coroutines.flow.asSharedFlow 7 | 8 | object GlobalScreenNavigation { 9 | 10 | private val _openScreenFlow = MutableSharedFlow() 11 | val openScreenFlow: SharedFlow get() = _openScreenFlow.asSharedFlow() 12 | 13 | suspend fun navigate(screen: Screen) { 14 | _openScreenFlow.emit(screen) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/utils/InstantExt.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.utils 2 | 3 | import com.zhangke.framework.date.InstantFormater 4 | import kotlinx.datetime.Clock 5 | import kotlinx.datetime.Instant 6 | 7 | fun getCurrentInstant(): Instant { 8 | return Clock.System.now() 9 | } 10 | 11 | fun getCurrentTimeMillis(): Long { 12 | return Clock.System.now().toEpochMilliseconds() 13 | } 14 | 15 | fun com.zhangke.framework.datetime.Instant.formatDefault(): String { 16 | return this.instant.formatDefault() 17 | } 18 | 19 | fun Instant.formatDefault(): String { 20 | return InstantFormater().formatToMediumDate(this) 21 | } 22 | -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/utils/ListStringConverter.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.utils 2 | 3 | import androidx.room.TypeConverter 4 | import com.zhangke.framework.architect.json.globalJson 5 | import kotlinx.serialization.encodeToString 6 | 7 | class ListStringConverter { 8 | 9 | @TypeConverter 10 | fun fromStringList(text: String): List { 11 | return globalJson.decodeFromString(text) 12 | } 13 | 14 | @TypeConverter 15 | fun toStringList(list: List): String { 16 | return globalJson.encodeToString(list) 17 | } 18 | } -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/utils/MediaFileHelper.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.utils 2 | 3 | import androidx.compose.runtime.staticCompositionLocalOf 4 | 5 | expect class MediaFileHelper { 6 | fun saveImageToGallery(url: String) 7 | 8 | fun saveVideoToGallery(url: String) 9 | } 10 | 11 | val LocalMediaFileHelper = 12 | staticCompositionLocalOf { error("No MediaFileHelper provided") } 13 | -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/utils/PlatformUriHelper.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.utils 2 | 3 | import androidx.compose.runtime.staticCompositionLocalOf 4 | import com.zhangke.framework.utils.ContentProviderFile 5 | import com.zhangke.framework.utils.PlatformUri 6 | 7 | expect class PlatformUriHelper { 8 | 9 | suspend fun read(uri: PlatformUri): ContentProviderFile? 10 | 11 | suspend fun readBytes(uri: PlatformUri): ByteArray? 12 | 13 | fun queryFileName(uri: PlatformUri): String? 14 | } 15 | 16 | val LocalPlatformUriHelper = staticCompositionLocalOf { 17 | error("No PlatformUriHelper provided") 18 | } -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/utils/RandomIdGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.utils 2 | 3 | expect class RandomIdGenerator() { 4 | 5 | fun generateId(): String 6 | } 7 | -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/utils/StorageHelper.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.utils 2 | 3 | import okio.Path 4 | 5 | expect class StorageHelper { 6 | val cacheDir: Path 7 | } 8 | -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/utils/ThumbnailHelper.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.utils 2 | 3 | import androidx.compose.runtime.staticCompositionLocalOf 4 | import androidx.compose.ui.graphics.ImageBitmap 5 | import com.zhangke.framework.utils.PlatformUri 6 | 7 | expect class ThumbnailHelper { 8 | fun getThumbnail(uri: PlatformUri): ImageBitmap? 9 | } 10 | 11 | val LocalThumbnailHelper = staticCompositionLocalOf { 12 | error("No ThumbnailHelper provided") 13 | } 14 | -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/utils/ToastHelper.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.utils 2 | 3 | import androidx.compose.runtime.staticCompositionLocalOf 4 | 5 | expect class ToastHelper { 6 | fun showToast(content: String) 7 | } 8 | 9 | val LocalToastHelper = staticCompositionLocalOf { 10 | error("No ToastHelper provided") 11 | } 12 | -------------------------------------------------------------------------------- /commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/utils/WebFingerConverter.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.utils 2 | 3 | import androidx.room.TypeConverter 4 | import com.zhangke.framework.utils.WebFinger 5 | 6 | class WebFingerConverter { 7 | 8 | @TypeConverter 9 | fun fromWebFinger(webFinger: WebFinger): String { 10 | return webFinger.toString() 11 | } 12 | 13 | @TypeConverter 14 | fun toWebFinger(text: String): WebFinger { 15 | return WebFinger.create(text)!! 16 | } 17 | } -------------------------------------------------------------------------------- /commonbiz/common/src/iosMain/kotlin/com/zhangke/fread/common/CommonActivityComponent.ios.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common 2 | 3 | import com.zhangke.fread.common.browser.ActivityBrowserLauncher 4 | import com.zhangke.fread.common.browser.IosActivityBrowserLauncher 5 | import me.tatarka.inject.annotations.Provides 6 | 7 | actual interface CommonActivityPlatformComponent { 8 | @Provides 9 | fun IosActivityBrowserLauncher.binds(): ActivityBrowserLauncher = this 10 | } 11 | -------------------------------------------------------------------------------- /commonbiz/common/src/iosMain/kotlin/com/zhangke/fread/common/page/BasePagerTabHookManager.ios.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.page 2 | 3 | internal actual fun findBasePagerTabImplementers(): List { 4 | // TODO: Not yet implemented 5 | return emptyList() 6 | } -------------------------------------------------------------------------------- /commonbiz/common/src/iosMain/kotlin/com/zhangke/fread/common/utils/MediaFileHelper.ios.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.utils 2 | 3 | import com.zhangke.fread.common.di.ApplicationScope 4 | import me.tatarka.inject.annotations.Inject 5 | 6 | @ApplicationScope 7 | actual class MediaFileHelper @Inject constructor() { 8 | actual fun saveImageToGallery(url: String) { 9 | TODO("Not yet implemented") 10 | } 11 | 12 | actual fun saveVideoToGallery(url: String) { 13 | TODO("Not yet implemented") 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /commonbiz/common/src/iosMain/kotlin/com/zhangke/fread/common/utils/RandomIdGenerator.ios.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.utils 2 | 3 | import kotlinx.datetime.Clock 4 | 5 | actual class RandomIdGenerator { 6 | 7 | actual fun generateId(): String { 8 | // TODO get device info for generate id 9 | return Clock.System.now().toString() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /commonbiz/common/src/iosMain/kotlin/com/zhangke/fread/common/utils/SystemUtils.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.utils 2 | 3 | import platform.Foundation.NSURL 4 | import platform.UIKit.UIApplication 5 | 6 | object SystemUtils { 7 | 8 | fun openAppStore(packageName: String) { 9 | val appStoreUrl = "https://apps.apple.com/cn/app/id${packageName}" 10 | val url = NSURL.URLWithString(appStoreUrl) 11 | if (url != null && UIApplication.sharedApplication.canOpenURL(url)) { 12 | UIApplication.sharedApplication.openURL(url) 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /commonbiz/common/src/iosMain/kotlin/com/zhangke/fread/common/utils/ThumbnailHelper.ios.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.utils 2 | 3 | import androidx.compose.ui.graphics.ImageBitmap 4 | import com.zhangke.framework.utils.PlatformUri 5 | import com.zhangke.fread.common.di.ApplicationScope 6 | import me.tatarka.inject.annotations.Inject 7 | 8 | @ApplicationScope 9 | actual class ThumbnailHelper @Inject constructor() { 10 | actual fun getThumbnail(uri: PlatformUri): ImageBitmap? { 11 | TODO("Not yet implemented") 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /commonbiz/common/src/iosMain/kotlin/com/zhangke/fread/common/utils/ToastHelper.ios.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.common.utils 2 | 3 | import com.zhangke.fread.common.di.ApplicationScope 4 | import me.tatarka.inject.annotations.Inject 5 | 6 | @ApplicationScope 7 | actual class ToastHelper @Inject constructor() { 8 | actual fun showToast(content: String) { 9 | TODO("Not yet implemented") 10 | } 11 | } -------------------------------------------------------------------------------- /commonbiz/sharedscreen/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /commonbiz/sharedscreen/src/androidMain/kotlin/com/zhangke/fread/commonbiz/shared/screen/ImageViewerScreen.android.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.commonbiz.shared.screen 2 | 3 | import com.seiko.imageloader.model.ImageResult 4 | import com.zhangke.framework.utils.aspectRatio 5 | 6 | internal actual fun ImageResult.aspectRatio(): Float? { 7 | return when (this) { 8 | is ImageResult.OfBitmap -> bitmap.width.toFloat() / bitmap.height 9 | is ImageResult.OfImage -> image.drawable.aspectRatio() 10 | else -> null 11 | } 12 | } -------------------------------------------------------------------------------- /commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/composable/OnBlogMediaClick.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.commonbiz.shared.composable 2 | 3 | import cafe.adriel.voyager.navigator.Navigator 4 | import com.zhangke.framework.voyager.TransparentNavigator 5 | import com.zhangke.fread.status.ui.image.BlogMediaClickEvent 6 | 7 | expect fun onStatusMediaClick( 8 | transparentNavigator: TransparentNavigator, 9 | navigator: Navigator, 10 | event: BlogMediaClickEvent, 11 | ) 12 | -------------------------------------------------------------------------------- /commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/composable/WebViewPreviewer.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.commonbiz.shared.composable 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | 6 | @Composable 7 | expect fun WebViewPreviewer( 8 | html: String, 9 | modifier: Modifier = Modifier, 10 | ) 11 | -------------------------------------------------------------------------------- /commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/feeds/CommonFeedsUiState.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.commonbiz.shared.feeds 2 | 3 | import com.zhangke.framework.utils.LoadState 4 | import com.zhangke.fread.status.model.StatusUiState 5 | 6 | data class CommonFeedsUiState( 7 | val feeds: List, 8 | val showPagingLoadingPlaceholder: Boolean, 9 | val pageErrorContent: Throwable?, 10 | val refreshing: Boolean, 11 | val loadMoreState: LoadState, 12 | ) 13 | -------------------------------------------------------------------------------- /commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/screen/publish/PublishBlogUiState.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.commonbiz.shared.screen.publish 2 | 3 | import androidx.compose.ui.text.input.TextFieldValue 4 | 5 | data class PublishBlogUiState( 6 | val content: TextFieldValue, 7 | ) 8 | -------------------------------------------------------------------------------- /commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/screen/publish/composable/BlogMediaAttachment.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.commonbiz.shared.screen.publish.composable 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | import com.zhangke.fread.commonbiz.shared.screen.publish.model.PublishBlogMediaAttachment 6 | 7 | @Composable 8 | fun BlogMediaAttachment( 9 | modifier: Modifier, 10 | attachment: PublishBlogMediaAttachment, 11 | ) { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/screen/publish/model/PublishBlogMediaAttachment.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.commonbiz.shared.screen.publish.model 2 | 3 | sealed interface PublishBlogMediaAttachment { 4 | 5 | data class Image(val files: List) : PublishBlogMediaAttachment 6 | 7 | data class Video(val file: PublishBlogMediaAttachmentFile) : PublishBlogMediaAttachment 8 | } 9 | 10 | data class PublishBlogMediaAttachmentFile( 11 | val uri: String, 12 | val description: String?, 13 | ) 14 | -------------------------------------------------------------------------------- /commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/usecase/RefactorToNewBlogUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.commonbiz.shared.usecase 2 | 3 | import com.zhangke.fread.status.status.model.Status 4 | import me.tatarka.inject.annotations.Inject 5 | 6 | class RefactorToNewBlogUseCase @Inject constructor() { 7 | 8 | operator fun invoke(status: Status): Status.NewBlog { 9 | return when (status) { 10 | is Status.NewBlog -> status 11 | is Status.Reblog -> Status.NewBlog( 12 | blog = status.reblog, 13 | ) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/usecase/RefactorToNewStatusUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.commonbiz.shared.usecase 2 | 3 | import com.zhangke.fread.status.model.StatusUiState 4 | import me.tatarka.inject.annotations.Inject 5 | 6 | class RefactorToNewStatusUseCase @Inject constructor( 7 | private val refactorToNewBlog: RefactorToNewBlogUseCase, 8 | ) { 9 | 10 | operator fun invoke(status: StatusUiState): StatusUiState { 11 | return status.copy( 12 | status = refactorToNewBlog(status.status), 13 | ) 14 | } 15 | } -------------------------------------------------------------------------------- /commonbiz/sharedscreen/src/iosMain/kotlin/com/zhangke/fread/commonbiz/shared/composable/OnBlogMediaClick.ios.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.commonbiz.shared.composable 2 | 3 | import cafe.adriel.voyager.navigator.Navigator 4 | import com.zhangke.framework.voyager.TransparentNavigator 5 | import com.zhangke.fread.status.ui.image.BlogMediaClickEvent 6 | 7 | actual fun onStatusMediaClick( 8 | transparentNavigator: TransparentNavigator, 9 | navigator: Navigator, 10 | event: BlogMediaClickEvent, 11 | ) { 12 | // TODO: Not implemented yet 13 | } 14 | -------------------------------------------------------------------------------- /commonbiz/sharedscreen/src/iosMain/kotlin/com/zhangke/fread/commonbiz/shared/composable/WebViewPreviewer.ios.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.commonbiz.shared.composable 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | 6 | @Composable 7 | actual fun WebViewPreviewer(html: String, modifier: Modifier) { 8 | // TODO: Not implemented yet 9 | } -------------------------------------------------------------------------------- /commonbiz/sharedscreen/src/iosMain/kotlin/com/zhangke/fread/commonbiz/shared/screen/ImageViewerScreen.ios.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.commonbiz.shared.screen 2 | 3 | import com.seiko.imageloader.model.ImageResult 4 | 5 | internal actual fun ImageResult.aspectRatio(): Float? { 6 | return when (this) { 7 | is ImageResult.OfBitmap -> bitmap.width.toFloat() / bitmap.height 8 | is ImageResult.OfImage -> image.width.toFloat() / image.height 9 | else -> null 10 | } 11 | } -------------------------------------------------------------------------------- /commonbiz/status-ui/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /commonbiz/status-ui/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xZhangKe/Fread/4a955bb14196ad5545a8bb2ab691b605bd8b288c/commonbiz/status-ui/consumer-rules.pro -------------------------------------------------------------------------------- /commonbiz/status-ui/src/androidMain/kotlin/com/zhangke/fread/status/ui/StatusPlaceHolder.preview.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.status.ui 2 | 3 | import androidx.compose.foundation.layout.fillMaxWidth 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | import androidx.compose.ui.tooling.preview.Preview 7 | import com.zhangke.framework.architect.theme.FreadTheme 8 | 9 | @Preview 10 | @Composable 11 | fun StatusPlaceHolderPreview() { 12 | FreadTheme { 13 | StatusPlaceHolder( 14 | modifier = Modifier.fillMaxWidth(), 15 | ) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /commonbiz/status-ui/src/androidMain/kotlin/com/zhangke/fread/status/ui/utils/ScreenSize.android.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.status.ui.utils 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.ReadOnlyComposable 5 | import androidx.compose.ui.platform.LocalConfiguration 6 | import androidx.compose.ui.unit.Dp 7 | import androidx.compose.ui.unit.dp 8 | 9 | @ReadOnlyComposable 10 | @Composable 11 | actual fun getScreenWidth(): Dp { 12 | val configuration = LocalConfiguration.current 13 | return configuration.screenWidthDp.dp 14 | } 15 | 16 | @ReadOnlyComposable 17 | @Composable 18 | actual fun getScreenHeight(): Dp { 19 | val configuration = LocalConfiguration.current 20 | return configuration.screenHeightDp.dp 21 | } -------------------------------------------------------------------------------- /commonbiz/status-ui/src/androidMain/kotlin/com/zhangke/fread/status/ui/video/LocalInlineVideoPlayer.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.status.ui.video 2 | 3 | import androidx.compose.runtime.ProvidableCompositionLocal 4 | import androidx.compose.runtime.staticCompositionLocalOf 5 | import androidx.media3.exoplayer.ExoPlayer 6 | 7 | val LocalInlineVideoPlayer: ProvidableCompositionLocal = 8 | staticCompositionLocalOf { null } 9 | 10 | fun provideExoPlayer(){ 11 | 12 | } 13 | -------------------------------------------------------------------------------- /commonbiz/status-ui/src/commonMain/composeResources/drawable/ic_mode_edit.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/BlogDivider.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.status.ui 2 | 3 | import androidx.compose.material3.HorizontalDivider 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | import androidx.compose.ui.unit.dp 7 | 8 | @Composable 9 | fun BlogDivider( 10 | modifier: Modifier = Modifier, 11 | ) { 12 | HorizontalDivider( 13 | modifier = modifier, 14 | thickness = 0.5.dp 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/source/StatusSourceUi.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.status.ui.source 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | import com.zhangke.fread.common.resources.logo 6 | import com.zhangke.fread.status.source.StatusSource 7 | 8 | @Composable 9 | fun StatusSourceUi( 10 | source: StatusSource, 11 | modifier: Modifier = Modifier, 12 | ) { 13 | SourceCommonUi( 14 | modifier = modifier, 15 | thumbnail = source.thumbnail.orEmpty(), 16 | title = source.name, 17 | subtitle = source.handle, 18 | description = source.description, 19 | protocolLogo = source.protocol.logo, 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/threads/ThreadsType.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.status.ui.threads 2 | 3 | enum class ThreadsType { 4 | 5 | NONE, 6 | 7 | /** 8 | * 第一个评论 9 | */ 10 | FIRST_ANCESTOR, 11 | 12 | /** 13 | * 帖子上级评论 14 | */ 15 | ANCESTOR, 16 | 17 | /** 18 | * 锚点帖子,且没有父级 19 | */ 20 | ANCHOR_FIRST, 21 | 22 | /** 23 | * 锚点帖子 24 | */ 25 | ANCHOR, 26 | } 27 | -------------------------------------------------------------------------------- /commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/utils/ScreenSize.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.status.ui.utils 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.ReadOnlyComposable 5 | import androidx.compose.ui.unit.Dp 6 | 7 | @Composable 8 | @ReadOnlyComposable 9 | expect fun getScreenWidth(): Dp 10 | 11 | @Composable 12 | @ReadOnlyComposable 13 | expect fun getScreenHeight(): Dp -------------------------------------------------------------------------------- /commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/video/BlogVideos.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.status.ui.video 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.zhangke.fread.status.blog.BlogMedia 5 | import com.zhangke.fread.status.ui.image.OnBlogMediaClick 6 | 7 | @Composable 8 | expect fun BlogVideos( 9 | mediaList: List, 10 | hideContent: Boolean, 11 | indexInList: Int, 12 | onMediaClick: OnBlogMediaClick, 13 | ) -------------------------------------------------------------------------------- /commonbiz/status-ui/src/commonMain/kotlin/org/burnoutcrew/reorderable/ItemPosition.kt: -------------------------------------------------------------------------------- 1 | package org.burnoutcrew.reorderable 2 | 3 | data class ItemPosition(val index: Int, val key: Any?) -------------------------------------------------------------------------------- /commonbiz/status-ui/src/iosMain/kotlin/com/zhangke/fread/status/ui/video/BlogVideos.ios.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.status.ui.video 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.zhangke.fread.status.blog.BlogMedia 5 | import com.zhangke.fread.status.ui.image.OnBlogMediaClick 6 | 7 | @Composable 8 | actual fun BlogVideos( 9 | mediaList: List, 10 | hideContent: Boolean, 11 | indexInList: Int, 12 | onMediaClick: OnBlogMediaClick, 13 | ) { 14 | // TODO: Not implemented yet 15 | } -------------------------------------------------------------------------------- /deleteuserdata.html: -------------------------------------------------------------------------------- 1 |

This document will tell you how to delete personal data in Fread.

2 |

1. Open the homepage of the app

3 |

2. Switch the bottom TAB to Profile

4 |

3. Click the more button of the account you logged in to

5 |

4. Click Log out

6 |

This way you can clear your login history.

7 |

If you want to clear all data, you can open the system settings page, select Fread and clear all data.

8 |

If you have any questions or suggestions about your personal data, do not hesitate to contact me at zhangkeport@gmail.com.

9 |

We do not save any of your personal information on the server, and your login information is all on the local disk of your device.

-------------------------------------------------------------------------------- /documents/architecture.xmind: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xZhangKe/Fread/4a955bb14196ad5545a8bb2ab691b605bd8b288c/documents/architecture.xmind -------------------------------------------------------------------------------- /fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | default_platform(:android) 2 | 3 | platform :android do 4 | desc "Build a release bundle (AAB)" 5 | lane :build do 6 | gradle( 7 | task: "bundle", 8 | build_type: "release" 9 | ) 10 | end 11 | 12 | end 13 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | Fread is a fediverse microblogging client that supports Mastodon, Bluesky, and RSS. 2 | With Fread, you can seamlessly access all three platforms within a single app. 3 | It delivers a consistent microblogging experience while preserving the unique features of each network. 4 | Most importantly, Fread allows you to create unified feeds that blend content across different protocols, breaking down barriers and strengthening decentralization. 5 | On top of that, Fread is designed with a focus on beautiful, comfortable UI/UX, offering a smooth and enjoyable experience. -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | A fediverse microblogging client supporting Mastodon, Bluesky, and RSS. -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/full_description.txt: -------------------------------------------------------------------------------- 1 | Fread 是一个联邦宇宙 Micro blogging 社交客户端,目前已经支持了 Mastodon、Bluesky、RSS 三种社交平台,这意味着你可以在同一个 App 中同时使用这三种社交平台。 2 | Fread 不仅提供了 Micro blogging 社交的一致性,也保持了不同平台的特色功能。 3 | 更重要的是,Fread 支持创建一个同时包含了三种来自不同平台的 Feeds 流,这打破了协议之间的壁垒,进一步增强了去中心化的能力。 4 | 另外 Fread 也专注于提供漂亮舒适的 UI/UX。 -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/short_description.txt: -------------------------------------------------------------------------------- 1 | 同时支持 Mastodon、Bluesky、RSS 的联邦宇宙客户端 -------------------------------------------------------------------------------- /feature/explore/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /feature/explore/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xZhangKe/Fread/4a955bb14196ad5545a8bb2ab691b605bd8b288c/feature/explore/consumer-rules.pro -------------------------------------------------------------------------------- /feature/explore/src/commonMain/kotlin/com/zhangke/fread/explore/ExplorerElements.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.explore 2 | 3 | object ExplorerElements { 4 | 5 | const val SEARCH = "explorerSearch" 6 | 7 | const val SWITCH_ACCOUNT = "explorerSwitchAccount" 8 | } 9 | -------------------------------------------------------------------------------- /feature/explore/src/commonMain/kotlin/com/zhangke/fread/explore/screens/search/SearchUiState.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.explore.screens.search 2 | 3 | class SearchUiState { 4 | } -------------------------------------------------------------------------------- /feature/explore/src/commonMain/kotlin/com/zhangke/fread/explore/screens/search/SearchViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.explore.screens.search 2 | 3 | import androidx.lifecycle.ViewModel 4 | import me.tatarka.inject.annotations.Inject 5 | 6 | class SearchViewModel @Inject constructor(): ViewModel() { 7 | 8 | } -------------------------------------------------------------------------------- /feature/explore/src/commonMain/kotlin/com/zhangke/fread/explore/screens/search/bar/SearchBarUiState.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.explore.screens.search.bar 2 | 3 | import com.zhangke.fread.common.status.model.SearchResultUiState 4 | import com.zhangke.fread.status.model.IdentityRole 5 | 6 | data class SearchBarUiState( 7 | val role: IdentityRole, 8 | val query: String, 9 | val resultList: List, 10 | ) 11 | -------------------------------------------------------------------------------- /feature/explore/src/commonMain/kotlin/com/zhangke/fread/explore/screens/search/platform/SearchedPlatformUiState.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.explore.screens.search.platform 2 | 3 | import com.zhangke.fread.status.search.SearchContentResult 4 | 5 | data class SearchedPlatformUiState( 6 | val searchedList: List, 7 | val searching: Boolean, 8 | ){ 9 | 10 | companion object{ 11 | 12 | fun default() = SearchedPlatformUiState( 13 | searchedList = emptyList(), 14 | searching = false, 15 | ) 16 | } 17 | } -------------------------------------------------------------------------------- /feature/feeds/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /feature/feeds/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xZhangKe/Fread/4a955bb14196ad5545a8bb2ab691b605bd8b288c/feature/feeds/consumer-rules.pro -------------------------------------------------------------------------------- /feature/feeds/src/commonMain/composeResources/drawable/ic_import.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /feature/feeds/src/commonMain/kotlin/com/zhangke/fread/feeds/FeedsScreenVisitor.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.feeds 2 | 3 | import cafe.adriel.voyager.core.screen.Screen 4 | import com.zhangke.fread.commonbiz.shared.IFeedsScreenVisitor 5 | import com.zhangke.fread.feeds.pages.manager.add.pre.PreAddFeedsScreen 6 | import me.tatarka.inject.annotations.Inject 7 | 8 | class FeedsScreenVisitor @Inject constructor() : IFeedsScreenVisitor { 9 | 10 | override fun getAddContentScreen(): Screen { 11 | return PreAddFeedsScreen() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /feature/feeds/src/commonMain/kotlin/com/zhangke/fread/feeds/pages/home/ContentHomeUiState.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.feeds.pages.home 2 | 3 | import com.zhangke.fread.status.model.FreadContent 4 | 5 | data class ContentHomeUiState( 6 | val currentPageIndex: Int, 7 | val loading: Boolean, 8 | val contentConfigList: List, 9 | ) { 10 | 11 | companion object { 12 | 13 | val default = ContentHomeUiState( 14 | currentPageIndex = 0, 15 | loading = true, 16 | contentConfigList = emptyList(), 17 | ) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /feature/feeds/src/commonMain/kotlin/com/zhangke/fread/feeds/pages/manager/add/mixed/AddMixedFeedsUiState.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.feeds.pages.manager.add.mixed 2 | 3 | import com.zhangke.fread.feeds.composable.StatusSourceUiState 4 | 5 | data class AddMixedFeedsUiState( 6 | val maxNameLength: Int, 7 | val sourceList: List, 8 | val sourceName: String, 9 | ) 10 | -------------------------------------------------------------------------------- /feature/feeds/src/commonMain/kotlin/com/zhangke/fread/feeds/pages/manager/edit/EditMixedContentUiState.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.feeds.pages.manager.edit 2 | 3 | import com.zhangke.fread.feeds.composable.StatusSourceUiState 4 | 5 | data class EditMixedContentUiState( 6 | val name: String, 7 | val sourceList: List, 8 | val errorMessage: String? = null, 9 | ) 10 | -------------------------------------------------------------------------------- /feature/feeds/src/commonMain/kotlin/com/zhangke/fread/feeds/pages/manager/importing/OpenDocumentContainer.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.feeds.pages.manager.importing 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.zhangke.framework.utils.PlatformUri 5 | 6 | @Composable 7 | expect fun OpenDocumentContainer( 8 | onResult: (PlatformUri) -> Unit, 9 | content: @Composable OpenDocumentContainerScope.() -> Unit 10 | ) 11 | 12 | expect class OpenDocumentContainerScope { 13 | fun launch() 14 | } -------------------------------------------------------------------------------- /feature/notifications/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /feature/notifications/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xZhangKe/Fread/4a955bb14196ad5545a8bb2ab691b605bd8b288c/feature/notifications/consumer-rules.pro -------------------------------------------------------------------------------- /feature/notifications/src/commonMain/composeResources/values-zh/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 通知 5 | 当前没有已登录账号 6 | 所有 7 | 提及 8 | -------------------------------------------------------------------------------- /feature/notifications/src/commonMain/composeResources/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Notifications 5 | No logged-in account currently 6 | All 7 | Mentions 8 | -------------------------------------------------------------------------------- /feature/notifications/src/commonMain/kotlin/com/zhangke/fread/feature/message/NotificationElements.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.feature.message 2 | 3 | object NotificationElements { 4 | 5 | const val SWITCH_ACCOUNT = "notificationSwitchAccount" 6 | } 7 | -------------------------------------------------------------------------------- /feature/notifications/src/commonMain/kotlin/com/zhangke/fread/feature/message/screens/home/NotificationsHomeUiState.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.feature.message.screens.home 2 | 3 | import com.zhangke.framework.composable.PagerTab 4 | import com.zhangke.fread.feature.message.screens.notification.NotificationTab 5 | import com.zhangke.fread.status.account.LoggedAccount 6 | 7 | data class NotificationsHomeUiState( 8 | val selectedAccount: LoggedAccount? = null, 9 | val accountList: List, 10 | ) { 11 | 12 | val tabs: List = accountList.map { NotificationTab(it) } 13 | } 14 | -------------------------------------------------------------------------------- /feature/profile/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /feature/profile/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xZhangKe/Fread/4a955bb14196ad5545a8bb2ab691b605bd8b288c/feature/profile/consumer-rules.pro -------------------------------------------------------------------------------- /feature/profile/src/commonMain/composeResources/drawable/af_dian.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xZhangKe/Fread/4a955bb14196ad5545a8bb2ab691b605bd8b288c/feature/profile/src/commonMain/composeResources/drawable/af_dian.webp -------------------------------------------------------------------------------- /feature/profile/src/commonMain/composeResources/drawable/ic_code.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /feature/profile/src/commonMain/composeResources/drawable/ic_ratting.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /feature/profile/src/commonMain/kotlin/com/zhangke/fread/profile/ProfileScreenVisitor.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.profile 2 | 3 | import cafe.adriel.voyager.core.screen.Screen 4 | import com.zhangke.fread.commonbiz.shared.IProfileScreenVisitor 5 | import com.zhangke.fread.profile.screen.donate.DonateScreen 6 | 7 | class ProfileScreenVisitor : IProfileScreenVisitor { 8 | 9 | override fun getDonateScreen(): Screen { 10 | return DonateScreen() 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /feature/profile/src/commonMain/kotlin/com/zhangke/fread/profile/screen/home/ProfileHomeUiState.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.profile.screen.home 2 | 3 | import com.zhangke.fread.status.account.LoggedAccount 4 | import com.zhangke.fread.status.platform.BlogPlatform 5 | 6 | data class ProfileHomeUiState( 7 | val accountDataList: List>>, 8 | ) 9 | 10 | data class ProfileAccountUiState( 11 | val account: LoggedAccount, 12 | val logged: Boolean, 13 | ) 14 | -------------------------------------------------------------------------------- /feature/profile/src/commonMain/kotlin/com/zhangke/fread/profile/screen/setting/SettingUiState.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.fread.profile.screen.setting 2 | 3 | import com.zhangke.fread.common.config.StatusContentSize 4 | import com.zhangke.fread.common.daynight.DayNightMode 5 | 6 | data class SettingUiState( 7 | val autoPlayInlineVideo: Boolean, 8 | val alwaysShowSensitiveContent: Boolean, 9 | val dayNightMode: DayNightMode, 10 | val settingInfo: String, 11 | val contentSize: StatusContentSize, 12 | val haveNewAppVersion: Boolean, 13 | ) 14 | -------------------------------------------------------------------------------- /framework/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /framework/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xZhangKe/Fread/4a955bb14196ad5545a8bb2ab691b605bd8b288c/framework/consumer-rules.pro -------------------------------------------------------------------------------- /framework/src/androidMain/kotlin/com/zhangke/framework/architect/http/HttpClientEngine.android.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.architect.http 2 | 3 | import io.ktor.client.engine.HttpClientEngine 4 | import io.ktor.client.engine.okhttp.OkHttp 5 | 6 | actual fun createHttpClientEngine(): HttpClientEngine { 7 | return OkHttp.create { 8 | preconfigured = GlobalOkHttpClient.client 9 | } 10 | } -------------------------------------------------------------------------------- /framework/src/androidMain/kotlin/com/zhangke/framework/architect/theme/FreadTheme.android.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.architect.theme 2 | 3 | import android.app.Activity 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.SideEffect 6 | import androidx.compose.ui.platform.LocalView 7 | import androidx.core.view.WindowCompat 8 | 9 | @Composable 10 | internal actual fun FreadPlatformTheme(darkTheme: Boolean) { 11 | val view = LocalView.current 12 | if (!view.isInEditMode) { 13 | SideEffect { 14 | val window = (view.context as Activity).window 15 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /framework/src/androidMain/kotlin/com/zhangke/framework/blurhash/BitmapExt.android.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.blurhash 2 | 3 | import android.graphics.Bitmap 4 | import androidx.compose.ui.graphics.ImageBitmap 5 | import androidx.compose.ui.graphics.asImageBitmap 6 | 7 | actual fun bitmapFromBuffer(buffer: IntArray, width: Int, height: Int): ImageBitmap { 8 | return Bitmap.createBitmap(buffer, width, height, Bitmap.Config.ARGB_8888).asImageBitmap() 9 | } 10 | -------------------------------------------------------------------------------- /framework/src/androidMain/kotlin/com/zhangke/framework/composable/AnimatableExt.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.composable 2 | 3 | import androidx.compose.animation.core.Animatable 4 | import androidx.compose.animation.core.AnimationVector1D 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.platform.LocalDensity 7 | import androidx.compose.ui.unit.Dp 8 | import com.zhangke.framework.utils.pxToDp 9 | 10 | val Animatable.dpValue: Dp 11 | @Composable get() { 12 | return value.pxToDp(LocalDensity.current) 13 | } 14 | -------------------------------------------------------------------------------- /framework/src/androidMain/kotlin/com/zhangke/framework/composable/TextString.android.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.composable 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.zhangke.framework.utils.appContext 5 | 6 | @Composable 7 | actual fun stringResource(resId: Int, vararg formatArgs: Any): String { 8 | return androidx.compose.ui.res.stringResource(resId, *formatArgs) 9 | } 10 | 11 | actual suspend fun TextString.getString(): String { 12 | return when (this) { 13 | is TextString.StringText -> string 14 | is TextString.ResourceText -> appContext.getString(resId, *formatArgs) 15 | is TextString.ComposeResourceText -> org.jetbrains.compose.resources.getString(res) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /framework/src/androidMain/kotlin/com/zhangke/framework/datetime/Instant.android.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.datetime 2 | 3 | import com.zhangke.framework.serialize.TimestampAsInstantSerializer 4 | import com.zhangke.framework.utils.Parcelize 5 | import kotlinx.serialization.Serializable 6 | 7 | //@Parcelize 8 | //@Serializable(with = TimestampAsInstantSerializer::class) 9 | //actual class Instant actual constructor( 10 | // actual val instant: kotlinx.datetime.Instant 11 | //) : java.io.Serializable -------------------------------------------------------------------------------- /framework/src/androidMain/kotlin/com/zhangke/framework/permission/PermissionUtils.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.permission 2 | 3 | import android.Manifest 4 | import android.content.Context 5 | import android.content.pm.PackageManager 6 | import android.os.Build 7 | import androidx.core.app.ActivityCompat 8 | 9 | fun Context.hasWriteStoragePermission(): Boolean { 10 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 11 | true 12 | } else { 13 | val permission = Manifest.permission.WRITE_EXTERNAL_STORAGE 14 | ActivityCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /framework/src/androidMain/kotlin/com/zhangke/framework/utils/DensityUtils.android.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.utils 2 | 3 | import android.content.Context 4 | import android.util.TypedValue 5 | 6 | context(Context) 7 | fun Int.dpToPx(): Int { 8 | return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), resources.displayMetrics) 9 | .toInt() 10 | } 11 | -------------------------------------------------------------------------------- /framework/src/androidMain/kotlin/com/zhangke/framework/utils/DrawableExt.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.utils 2 | 3 | import android.graphics.drawable.Drawable 4 | 5 | fun Drawable.aspectRatio(): Float { 6 | return intrinsicWidth.toFloat() / intrinsicHeight.toFloat() 7 | } 8 | -------------------------------------------------------------------------------- /framework/src/androidMain/kotlin/com/zhangke/framework/utils/ImageLoaderUtils.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.utils 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.drawable.BitmapDrawable 5 | import com.seiko.imageloader.model.ImageResult 6 | 7 | fun ImageResult.asBitmapOrNull(): Bitmap? = when (this) { 8 | is ImageResult.OfBitmap -> bitmap 9 | is ImageResult.OfImage -> image.drawable.let { it as? BitmapDrawable }?.bitmap 10 | else -> null 11 | } 12 | -------------------------------------------------------------------------------- /framework/src/androidMain/kotlin/com/zhangke/framework/utils/PlatformIgnoredOnParcel.android.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.utils 2 | 3 | import kotlinx.parcelize.IgnoredOnParcel 4 | 5 | actual typealias PlatformIgnoredOnParcel = IgnoredOnParcel -------------------------------------------------------------------------------- /framework/src/androidMain/kotlin/com/zhangke/framework/utils/PlatformParcelable.android.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.utils 2 | 3 | actual typealias PlatformParcelable = android.os.Parcelable -------------------------------------------------------------------------------- /framework/src/androidMain/kotlin/com/zhangke/framework/utils/PlatformTransient.android.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.utils 2 | 3 | actual typealias PlatformTransient = kotlin.jvm.Transient 4 | -------------------------------------------------------------------------------- /framework/src/androidMain/kotlin/com/zhangke/framework/utils/PlatformUri.android.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.utils 2 | 3 | import android.net.Uri 4 | import androidx.core.net.toUri 5 | import com.eygraber.uri.toUri 6 | 7 | fun PlatformUri.toAndroidUri(): Uri = toString().toUri() 8 | 9 | fun Uri.toPlatformUri(): PlatformUri = toUri() -------------------------------------------------------------------------------- /framework/src/androidMain/kotlin/com/zhangke/framework/utils/Serializable.android.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.utils 2 | 3 | actual typealias PlatformSerializable = java.io.Serializable -------------------------------------------------------------------------------- /framework/src/androidUnitTest/kotlin/com/zhangke/framework/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /framework/src/androidUnitTest/kotlin/com/zhangke/framework/RegexFactoryTest.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework 2 | 3 | import org.junit.Test 4 | 5 | class RegexFactoryTest { 6 | 7 | } -------------------------------------------------------------------------------- /framework/src/androidUnitTest/kotlin/com/zhangke/framework/feeds/fetcher/FeedsFetcherTest.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.feeds.fetcher 2 | 3 | class FeedsFetcherTest { 4 | 5 | 6 | } -------------------------------------------------------------------------------- /framework/src/androidUnitTest/kotlin/com/zhangke/framework/utils/RegexFactoryTest.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.utils 2 | 3 | import org.junit.Test 4 | 5 | class RegexFactoryTest { 6 | 7 | @Test 8 | fun test(){ 9 | val find = RegexFactory.domainRegex.find("https://m3.material.io") 10 | println(find?.value) 11 | } 12 | } -------------------------------------------------------------------------------- /framework/src/androidUnitTest/kotlin/com/zhangke/framework/utils/StorageSizeTest.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.utils 2 | 3 | import org.junit.Test 4 | 5 | class StorageSizeTest { 6 | 7 | @Test 8 | fun test() { 9 | val bytes = 1L * 1024 * 1024 * 1024 10 | val storageSize = StorageSize(bytes) 11 | println(storageSize.bytes) 12 | println(storageSize.KB) 13 | println(storageSize.MB) 14 | println(storageSize.GB) 15 | } 16 | } -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/architect/coroutines/ApplicationScope.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.architect.coroutines 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlinx.coroutines.Dispatchers 5 | import kotlinx.coroutines.SupervisorJob 6 | 7 | val ApplicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) 8 | -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/architect/coroutines/Flows.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.architect.coroutines 2 | 3 | import androidx.lifecycle.LifecycleOwner 4 | import androidx.lifecycle.lifecycleScope 5 | import kotlinx.coroutines.flow.Flow 6 | import kotlinx.coroutines.flow.FlowCollector 7 | import kotlinx.coroutines.launch 8 | 9 | /** 10 | * Created by ZhangKe on 2022/10/14. 11 | */ 12 | 13 | fun Flow.collectWithLifecycle(lifecycle: LifecycleOwner, collector: FlowCollector) { 14 | lifecycle.lifecycleScope 15 | .launch { 16 | collect(collector) 17 | } 18 | } -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/architect/json/JsonModuleBuilder.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.architect.json 2 | 3 | import kotlinx.serialization.modules.SerializersModuleBuilder 4 | 5 | interface JsonModuleBuilder { 6 | 7 | fun SerializersModuleBuilder.buildSerializersModule() 8 | } 9 | -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/blurhash/BitmapExt.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.blurhash 2 | 3 | import androidx.compose.ui.graphics.ImageBitmap 4 | 5 | expect fun bitmapFromBuffer(buffer: IntArray, width: Int, height: Int): ImageBitmap 6 | -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/composable/AppBar.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.composable 2 | 3 | import androidx.compose.ui.unit.dp 4 | 5 | val AppBarHeight = 56.dp 6 | -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/composable/DpSaver.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.composable 2 | 3 | import androidx.compose.runtime.saveable.Saver 4 | import androidx.compose.ui.unit.Dp 5 | 6 | object DpSaver { 7 | 8 | val Saver: Saver = Saver( 9 | save = { 10 | it.value 11 | }, 12 | restore = { 13 | Dp(it) 14 | } 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/composable/FlowUtils.android.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.composable 2 | 3 | import kotlinx.coroutines.flow.MutableSharedFlow 4 | 5 | suspend fun MutableSharedFlow.tryEmitException(exception: Throwable) { 6 | exception.message 7 | ?.takeIf { it.isNotEmpty() } 8 | ?.let { textOf(it) } 9 | ?.let { 10 | this.emit(it) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/composable/Keyboard.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.composable 2 | 3 | import androidx.compose.foundation.layout.WindowInsets 4 | import androidx.compose.foundation.layout.ime 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.State 7 | import androidx.compose.runtime.rememberUpdatedState 8 | import androidx.compose.ui.platform.LocalDensity 9 | 10 | @Composable 11 | fun keyboardAsState(): State { 12 | val isImeVisible = WindowInsets.ime.getBottom(LocalDensity.current) > 0 13 | return rememberUpdatedState(isImeVisible) 14 | } 15 | -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/composable/LazyListStateUtils.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.composable 2 | 3 | import androidx.compose.foundation.lazy.LazyListState 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.derivedStateOf 6 | import androidx.compose.runtime.getValue 7 | import androidx.compose.runtime.remember 8 | 9 | @Composable 10 | fun canScrollBackward(state: LazyListState): Boolean { 11 | val canScrollBackward by remember { 12 | derivedStateOf { 13 | state.firstVisibleItemIndex != 0 || state.firstVisibleItemScrollOffset != 0 14 | } 15 | } 16 | return canScrollBackward 17 | } 18 | -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/composable/LocalSnackMessage.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.composable 2 | 3 | import androidx.compose.material3.SnackbarHostState 4 | import androidx.compose.runtime.ProvidableCompositionLocal 5 | import androidx.compose.runtime.staticCompositionLocalOf 6 | 7 | val LocalSnackbarHostState: ProvidableCompositionLocal = 8 | staticCompositionLocalOf { null } 9 | -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/composable/NestedScrollConnection.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.composable 2 | 3 | import androidx.compose.ui.Modifier 4 | import androidx.compose.ui.input.nestedscroll.NestedScrollConnection 5 | import androidx.compose.ui.input.nestedscroll.nestedScroll 6 | 7 | fun Modifier.applyNestedScrollConnection( 8 | nestedScrollConnection: NestedScrollConnection?, 9 | ): Modifier { 10 | if (nestedScrollConnection == null) return this 11 | return Modifier.nestedScroll(nestedScrollConnection) then this 12 | } 13 | -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/composable/NoRippleClick.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.composable 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.interaction.MutableInteractionSource 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.remember 7 | import androidx.compose.ui.Modifier 8 | 9 | @Composable 10 | fun Modifier.noRippleClick(enabled: Boolean = true, onClick: () -> Unit): Modifier { 11 | return Modifier.clickable( 12 | enabled = enabled, 13 | interactionSource = remember { MutableInteractionSource() }, 14 | onClick = onClick, 15 | indication = null, 16 | ) then this 17 | } 18 | -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/composable/Offset.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.composable 2 | 3 | import androidx.compose.foundation.layout.offset 4 | import androidx.compose.ui.Modifier 5 | import androidx.compose.ui.composed 6 | import androidx.compose.ui.geometry.Offset 7 | import androidx.compose.ui.platform.LocalDensity 8 | import com.zhangke.framework.utils.pxToDp 9 | 10 | fun Modifier.offset(offset: Offset): Modifier = composed { 11 | val density = LocalDensity.current 12 | Modifier.offset( 13 | x = offset.x.pxToDp(density), 14 | y = offset.y.pxToDp(density), 15 | ) then this 16 | } 17 | 18 | val Offset.isZero: Boolean get() = x == 0F && y == 0F 19 | -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/composable/SystemUi.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.composable 2 | 3 | import androidx.compose.foundation.layout.WindowInsets 4 | import androidx.compose.foundation.layout.navigationBars 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.platform.LocalDensity 7 | import androidx.compose.ui.unit.Dp 8 | import com.zhangke.framework.utils.pxToDp 9 | 10 | @Composable 11 | fun getNavigationBarHeight(insets: WindowInsets = WindowInsets.navigationBars): Dp { 12 | val density = LocalDensity.current 13 | return insets.getTop(density).pxToDp(density) 14 | } 15 | -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/composable/VelocityExt.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.composable 2 | 3 | import androidx.compose.ui.geometry.Offset 4 | import androidx.compose.ui.unit.Velocity 5 | 6 | fun Velocity.toOffset() = Offset(x = x, y = y) -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/composable/pick/PickVisualMediaLauncherContainer.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.composable.pick 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.zhangke.framework.utils.PlatformUri 5 | 6 | @Composable 7 | expect fun PickVisualMediaLauncherContainer( 8 | onResult: (List) -> Unit, 9 | maxItems: Int = 1, 10 | content: @Composable PickVisualMediaLauncherContainerScope.() -> Unit, 11 | ) 12 | 13 | expect class PickVisualMediaLauncherContainerScope { 14 | 15 | fun launchImage() 16 | 17 | fun launchMedia() 18 | 19 | fun launchVideo() 20 | } 21 | -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/composable/theme/TopAppBar.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.composable.theme 2 | 3 | import androidx.compose.ui.unit.dp 4 | 5 | object TopAppBarDefault { 6 | 7 | val StartPadding = 16.dp 8 | 9 | val EndPadding = 16.dp 10 | 11 | val TopBarHeight = 56.dp 12 | } 13 | -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/composable/video/VideoPlayer.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.composable.video 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | import com.zhangke.framework.utils.PlatformUri 6 | 7 | @Composable 8 | expect fun VideoPlayer( 9 | uri: PlatformUri, 10 | playWhenReady: Boolean, 11 | modifier: Modifier = Modifier, 12 | state: VideoState = rememberVideoPlayerState(), 13 | useController: Boolean = false, 14 | ) -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/coroutines/JobExt.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.coroutines 2 | 3 | import kotlinx.coroutines.CancellationException 4 | import kotlinx.coroutines.Job 5 | 6 | fun Job.invokeOnCancel(block: (CancellationException) -> Unit) { 7 | invokeOnCompletion { 8 | if (it is CancellationException) { 9 | block(it) 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/date/InstantFormater.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.date 2 | 3 | import kotlinx.datetime.Instant 4 | 5 | expect class InstantFormater() { 6 | 7 | fun formatToMediumDate(instant: Instant): String 8 | } 9 | -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/datetime/Instant.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.datetime 2 | 3 | import cafe.adriel.voyager.core.lifecycle.JavaSerializable 4 | import com.zhangke.framework.serialize.TimestampAsInstantSerializer 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable(with = TimestampAsInstantSerializer::class) 8 | data class Instant(val epochMillis: Long) : JavaSerializable { 9 | 10 | val instant: kotlinx.datetime.Instant 11 | get() = kotlinx.datetime.Instant.fromEpochMilliseconds(epochMillis) 12 | } 13 | 14 | fun Instant(instant: kotlinx.datetime.Instant): Instant { 15 | return Instant(instant.toEpochMilliseconds()) 16 | } 17 | -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/imageloader/ImageLoaderUtils.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.imageloader 2 | 3 | import com.seiko.imageloader.ImageLoader 4 | import com.seiko.imageloader.model.ImageRequest 5 | import com.seiko.imageloader.model.ImageResult 6 | 7 | suspend fun ImageLoader.executeSafety(request: ImageRequest): ImageResult { 8 | return try { 9 | this.execute(request) 10 | } catch (e: Throwable) { 11 | ImageResult.OfError(e) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/ktx/FlowExt.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.ktx 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlinx.coroutines.flow.SharingStarted 5 | import kotlinx.coroutines.flow.StateFlow 6 | import kotlinx.coroutines.flow.map 7 | import kotlinx.coroutines.flow.stateIn 8 | 9 | fun StateFlow.map( 10 | coroutineScope: CoroutineScope, 11 | mapper: (value: T) -> M 12 | ): StateFlow = map { mapper(it) }.stateIn( 13 | coroutineScope, 14 | SharingStarted.Eagerly, 15 | mapper(value) 16 | ) 17 | -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/ktx/LazyBackingFieldDelegate.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.ktx 2 | 3 | import kotlin.reflect.KProperty 4 | 5 | class LazyBackingFieldDelegate(private val provider: () -> T) { 6 | 7 | private var backingField: T? = null 8 | 9 | operator fun getValue(thisRef: Any?, property: KProperty<*>): T { 10 | if (backingField == null) { 11 | backingField = provider() 12 | } 13 | return backingField!! 14 | } 15 | 16 | operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { 17 | backingField = value 18 | } 19 | } -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/ktx/StringExt.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.ktx 2 | 3 | inline fun String?.ifNullOrEmpty(block: () -> String): String { 4 | if (this == null) return block() 5 | return ifEmpty(block) 6 | } 7 | -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/lifecycle/ContainerViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.lifecycle 2 | 3 | import androidx.lifecycle.ViewModel 4 | 5 | abstract class ContainerViewModel : 6 | ViewModel() { 7 | 8 | abstract fun createSubViewModel(params: P): T 9 | 10 | private val subViewModelStore = mutableMapOf() 11 | 12 | protected fun obtainSubViewModel(params: P): T { 13 | return subViewModelStore.getOrPut(params.key) { 14 | createSubViewModel(params) 15 | }.also { addCloseable(it) } 16 | } 17 | 18 | abstract class SubViewModelParams { 19 | 20 | abstract val key: String 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/lifecycle/SubViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.lifecycle 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlinx.coroutines.Dispatchers 5 | import kotlinx.coroutines.SupervisorJob 6 | import kotlinx.coroutines.cancel 7 | 8 | import kotlin.coroutines.CoroutineContext 9 | 10 | abstract class SubViewModel : AutoCloseable, CoroutineScope { 11 | 12 | final override val coroutineContext: CoroutineContext = SupervisorJob() + Dispatchers.Main.immediate 13 | 14 | val viewModelScope = CoroutineScope(coroutineContext) 15 | 16 | override fun close() { 17 | coroutineContext.cancel() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/module/ModuleStartup.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.module 2 | 3 | interface ModuleStartup { 4 | 5 | fun onAppCreate() 6 | } 7 | -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/network/GlobalRoutes.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.network 2 | 3 | object GlobalRoutes { 4 | private const val SCHEME = "fread" 5 | const val ROOT_PREFIX = "$SCHEME://" 6 | } -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/network/HttpScheme.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.network 2 | 3 | object HttpScheme { 4 | 5 | const val HTTP = "http://" 6 | const val HTTPS = "https://" 7 | 8 | fun validate(scheme: String): Boolean { 9 | val fixedScheme = scheme.lowercase() 10 | return fixedScheme == HTTP || fixedScheme == HTTPS 11 | } 12 | } 13 | 14 | fun String.addProtocolIfNecessary(): String { 15 | if (this.contains("://")) return this 16 | return "${HttpScheme.HTTPS}$this" 17 | } 18 | -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/opml/OpmlOutline.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.opml 2 | 3 | data class OpmlOutline( 4 | val title: String, 5 | val xmlUrl: String, 6 | val children: List, 7 | ) 8 | -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/permission/RequireLocalStoragePermission.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.permission 2 | 3 | import androidx.compose.runtime.Composable 4 | 5 | @Composable 6 | expect fun RequireLocalStoragePermission( 7 | onPermissionGranted: suspend () -> Unit, 8 | onPermissionDenied: suspend (() -> Unit) = {}, 9 | ) -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/security/Md5.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.security 2 | 3 | import io.ktor.utils.io.core.toByteArray 4 | import okio.ByteString 5 | 6 | object Md5 { 7 | 8 | fun md5(input: String): String { 9 | return ByteString.of(*input.toByteArray()).md5().hex() 10 | } 11 | } -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/toast/Toast.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.toast 2 | 3 | expect fun toast(message: String?) -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/utils/AspectRatio.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.utils 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Parcelize 6 | @Serializable 7 | data class AspectRatio( 8 | val width: Long, 9 | val height: Long, 10 | ) : PlatformSerializable, PlatformParcelable { 11 | 12 | val ratio: Float get() = width.toFloat() / height 13 | 14 | init { 15 | require(width >= 1) { 16 | "width must be >= 1, but was $width" 17 | } 18 | require(height >= 1) { 19 | "height must be >= 1, but was $height" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/utils/ContentProviderFile.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.utils 2 | 3 | data class ContentProviderFile( 4 | val uri: PlatformUri, 5 | val fileName: String, 6 | val size: StorageSize, 7 | val mimeType: String, 8 | private val streamProvider: () -> ByteArray?, 9 | ) { 10 | fun readBytes(): ByteArray? { 11 | return streamProvider() 12 | } 13 | 14 | val isVideo: Boolean get() = mimeType.contains("video") 15 | } 16 | 17 | -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/utils/FloatExt.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.utils 2 | 3 | import kotlin.math.abs 4 | 5 | fun Float.equalsExactly(target: Float): Boolean { 6 | return abs(target - this) <= 0.000001F 7 | } 8 | -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/utils/Handle.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.utils 2 | 3 | fun String.prettyHandle(): String = if (this.startsWith('@')) this else "@$this" 4 | -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/utils/LanguageUtils.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.utils 2 | 3 | expect object LanguageUtils { 4 | fun getAllLanguages(): List 5 | } 6 | 7 | expect class Locale 8 | 9 | expect val Locale.languageCode: String 10 | 11 | expect val Locale.isO3LanguageCode: String 12 | 13 | expect fun Locale.getDisplayName(displayLocale: Locale): String 14 | 15 | expect fun initLocale(language: String): Locale 16 | 17 | expect fun getDefaultLocale(): Locale 18 | -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/utils/LoadState.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.utils 2 | 3 | import com.zhangke.framework.composable.TextString 4 | 5 | 6 | sealed interface LoadState { 7 | 8 | val loading: Boolean 9 | get() = this is Loading 10 | 11 | data object Idle : LoadState 12 | 13 | data object Loading : LoadState 14 | 15 | data class Failed(val message: TextString?) : LoadState 16 | } 17 | -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/utils/Log.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.utils 2 | 3 | import co.touchlab.kermit.Logger 4 | import co.touchlab.kermit.loggerConfigInit 5 | import co.touchlab.kermit.platformLogWriter 6 | 7 | object Log { 8 | 9 | private val log = Logger( 10 | loggerConfigInit(platformLogWriter()), 11 | "Fread", 12 | ) 13 | 14 | fun d(tag: String, message: () -> String) { 15 | log.d(tag = tag, message = message) 16 | } 17 | 18 | fun i(tag: String, message: () -> String) { 19 | log.i(tag = tag, message = message) 20 | } 21 | } -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/utils/Parcelize.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.utils 2 | 3 | @Target(AnnotationTarget.CLASS) 4 | @Retention(AnnotationRetention.BINARY) 5 | annotation class Parcelize 6 | -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/utils/PlatformIgnoredOnParcel.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.utils 2 | 3 | expect annotation class PlatformIgnoredOnParcel() -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/utils/PlatformParcelable.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.utils 2 | 3 | expect interface PlatformParcelable -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/utils/PlatformSerializable.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.utils 2 | 3 | expect interface PlatformSerializable -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/utils/PlatformTransient.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.utils 2 | 3 | @Target(AnnotationTarget.PROPERTY) 4 | expect annotation class PlatformTransient() -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/utils/PlatformUri.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.utils 2 | 3 | import com.eygraber.uri.Uri 4 | 5 | typealias PlatformUri = Uri 6 | 7 | fun String.toPlatformUri(): PlatformUri { 8 | return Uri.parse(this) 9 | } 10 | -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/utils/RegexFactory.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.utils 2 | 3 | object RegexFactory { 4 | 5 | val domainRegex = 6 | "^(?=^.{3,255}$)[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+$".toRegex() 7 | 8 | val didRegex = "^did:[a-z0-9]+:[a-zA-Z0-9._%-]+$".toRegex() 9 | } 10 | -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/utils/ResultExt.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.utils 2 | 3 | fun List>>.collect(): Result> { 4 | if (isNotEmpty() && any { it.isSuccess }.not()) { 5 | return first() 6 | } 7 | return mapNotNull { 8 | it.getOrNull() 9 | }.reduce { list1, list2 -> 10 | mutableListOf().apply { 11 | addAll(list1) 12 | addAll(list2) 13 | } 14 | }.let { Result.success(it) } 15 | } 16 | 17 | fun Result.exceptionOrThrow(): Throwable { 18 | return exceptionOrNull() ?: throw IllegalStateException("Result is success!") 19 | } 20 | 21 | fun Result<*>.ignoreContent(): Result = map {} 22 | -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/utils/Standard.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.utils 2 | 3 | inline fun T.maybe(predication: Boolean, block: (T) -> T): T { 4 | return if (predication) block(this) else this 5 | } 6 | -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/utils/VideoUtils.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.utils 2 | 3 | expect class VideoUtils() { 4 | 5 | fun getVideoAspect(uri: String): AspectRatio? 6 | } 7 | -------------------------------------------------------------------------------- /framework/src/commonMain/kotlin/com/zhangke/framework/voyager/RootNavigator.kt: -------------------------------------------------------------------------------- 1 | package com.zhangke.framework.voyager 2 | 3 | import cafe.adriel.voyager.core.annotation.InternalVoyagerApi 4 | import cafe.adriel.voyager.navigator.Navigator 5 | 6 | const val ROOT_NAVIGATOR_KEY = "com.zhangke.framework.voyager.ROOT_NAVIGATOR" 7 | 8 | @OptIn(InternalVoyagerApi::class) 9 | val Navigator.rootNavigator: Navigator 10 | get() { 11 | var rootNavigator = this 12 | while (rootNavigator.key != ROOT_NAVIGATOR_KEY) { 13 | rootNavigator = rootNavigator.parent ?: break 14 | } 15 | return rootNavigator 16 | } 17 | -------------------------------------------------------------------------------- /framework/src/commonMain/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /framework/src/commonMain/res/values/ids.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /framework/src/commonMain/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |