Showing preview only (5,529K chars total). Download the full file or copy to clipboard to get everything.
Repository: fantasytyx/bv
Branch: develop
Commit: b84935bf29c9
Files: 945
Total size: 4.8 MB
Directory structure:
gitextract_fcqp1f9p/
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml
│ │ ├── config.yml
│ │ └── feature_request.yml
│ └── workflows/
│ ├── alpha.yml
│ ├── alpha_build_manually_without_sign.yml
│ ├── auto_close_issues.yml
│ ├── close_inactive_issues.yml
│ ├── features.yml
│ └── release.yml
├── .gitignore
├── .gitmodules
├── CHANGELOG.md
├── LICENSE
├── README.md
├── app/
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── compose_compiler_config.conf
│ ├── mobile/
│ │ ├── .gitignore
│ │ ├── build.gradle.kts
│ │ ├── consumer-rules.pro
│ │ ├── proguard-rules.pro
│ │ └── src/
│ │ └── main/
│ │ ├── AndroidManifest.xml
│ │ ├── kotlin/
│ │ │ ├── com/
│ │ │ │ └── origeek/
│ │ │ │ └── imageViewer/
│ │ │ │ ├── gallery/
│ │ │ │ │ ├── ImageGallery.kt
│ │ │ │ │ └── ImagePager.kt
│ │ │ │ ├── previewer/
│ │ │ │ │ ├── ImagePreviewer.kt
│ │ │ │ │ ├── ImageTransform.kt
│ │ │ │ │ ├── ImageViewerContainer.kt
│ │ │ │ │ ├── PreviewerPagerState.kt
│ │ │ │ │ ├── PreviewerTransformState.kt
│ │ │ │ │ └── PreviewerVerticalDragState.kt
│ │ │ │ ├── util/
│ │ │ │ │ └── Ticket.kt
│ │ │ │ └── viewer/
│ │ │ │ ├── ImageComposeCanvas.kt
│ │ │ │ ├── ImageComposeOrigin.kt
│ │ │ │ └── ImageViewer.kt
│ │ │ └── dev/
│ │ │ └── aaa1115910/
│ │ │ └── bv/
│ │ │ └── mobile/
│ │ │ ├── activities/
│ │ │ │ ├── DynamicDetailActivity.kt
│ │ │ │ ├── FavoriteActivity.kt
│ │ │ │ ├── FollowingSeasonActivity.kt
│ │ │ │ ├── FollowingUserActivity.kt
│ │ │ │ ├── HistoryActivity.kt
│ │ │ │ ├── IntentHandlerActivity.kt
│ │ │ │ ├── LoginActivity.kt
│ │ │ │ ├── MainActivity.kt
│ │ │ │ ├── QrTokenResultActivity.kt
│ │ │ │ ├── SettingsActivity.kt
│ │ │ │ ├── UserSpaceActivity.kt
│ │ │ │ └── VideoPlayerActivity.kt
│ │ │ ├── component/
│ │ │ │ ├── home/
│ │ │ │ │ ├── SearchBar.kt
│ │ │ │ │ ├── UserDialog.kt
│ │ │ │ │ └── dynamic/
│ │ │ │ │ ├── DynamicItem.kt
│ │ │ │ │ └── DynamicUserItem.kt
│ │ │ │ ├── player/
│ │ │ │ │ └── VideoPlayerPages.kt
│ │ │ │ ├── preferences/
│ │ │ │ │ ├── PreferenceGroup.kt
│ │ │ │ │ ├── PreferencesPreview.kt
│ │ │ │ │ └── items/
│ │ │ │ │ ├── BaseListItem.kt
│ │ │ │ │ ├── ListItemPreference.kt
│ │ │ │ │ ├── RadioPreference.kt
│ │ │ │ │ ├── SwitchPreference.kt
│ │ │ │ │ └── TextPreference.kt
│ │ │ │ ├── reply/
│ │ │ │ │ ├── CommentItem.kt
│ │ │ │ │ ├── Comments.kt
│ │ │ │ │ ├── Replies.kt
│ │ │ │ │ └── ReplySheetScaffold.kt
│ │ │ │ ├── search/
│ │ │ │ │ ├── PgcCard.kt
│ │ │ │ │ ├── UgcCard.kt
│ │ │ │ │ └── UserCard.kt
│ │ │ │ ├── settings/
│ │ │ │ │ └── UpdateDialog.kt
│ │ │ │ ├── user/
│ │ │ │ │ └── UserAvatar.kt
│ │ │ │ └── videocard/
│ │ │ │ ├── RelatedVideoItem.kt
│ │ │ │ ├── SeasonCard.kt
│ │ │ │ ├── SmallVideoCard.kt
│ │ │ │ ├── UpIcon.kt
│ │ │ │ └── UpSpaceVideoItem.kt
│ │ │ ├── screen/
│ │ │ │ ├── DynamicDetailScreen.kt
│ │ │ │ ├── FavoriteScreen.kt
│ │ │ │ ├── FollowingSeasonScreen.kt
│ │ │ │ ├── FollowingUserScreen.kt
│ │ │ │ ├── HistoryScreen.kt
│ │ │ │ ├── LoginScreen.kt
│ │ │ │ ├── MobileMainScreen.kt
│ │ │ │ ├── QrTokenResultScreen.kt
│ │ │ │ ├── RegionBlockScreen.kt
│ │ │ │ ├── UserSpaceScreen.kt
│ │ │ │ ├── VideoPlayerScreen.kt
│ │ │ │ ├── home/
│ │ │ │ │ ├── DynamicScreen.kt
│ │ │ │ │ ├── HomeScreen.kt
│ │ │ │ │ ├── SearchScreen.kt
│ │ │ │ │ ├── home/
│ │ │ │ │ │ ├── PopularPage.kt
│ │ │ │ │ │ └── RcmdPage.kt
│ │ │ │ │ └── search/
│ │ │ │ │ ├── SearchInput.kt
│ │ │ │ │ └── SearchResult.kt
│ │ │ │ └── settings/
│ │ │ │ ├── SettingsCategories.kt
│ │ │ │ ├── SettingsDetails.kt
│ │ │ │ ├── SettingsScreen.kt
│ │ │ │ └── details/
│ │ │ │ ├── AboutContent.kt
│ │ │ │ ├── AdvanceContent.kt
│ │ │ │ ├── DebugContent.kt
│ │ │ │ └── PlayContent.kt
│ │ │ └── theme/
│ │ │ └── Theme.kt
│ │ └── res/
│ │ └── values/
│ │ ├── strings.xml
│ │ └── themes.xml
│ ├── proguard-rules.pro
│ ├── shared/
│ │ ├── .gitignore
│ │ ├── build.gradle.kts
│ │ ├── consumer-rules.pro
│ │ ├── proguard-rules.pro
│ │ ├── schemas/
│ │ │ └── dev.aaa1115910.bv.dao.AppDatabase/
│ │ │ ├── 1.json
│ │ │ ├── 2.json
│ │ │ └── 3.json
│ │ └── src/
│ │ ├── debug/
│ │ │ ├── AndroidManifest.xml
│ │ │ └── res/
│ │ │ └── values/
│ │ │ └── strings.xml
│ │ ├── main/
│ │ │ ├── AndroidManifest.xml
│ │ │ ├── kotlin/
│ │ │ │ ├── coil/
│ │ │ │ │ └── transform/
│ │ │ │ │ └── BlurTransformation.kt
│ │ │ │ ├── de/
│ │ │ │ │ └── schnettler/
│ │ │ │ │ └── datastore/
│ │ │ │ │ └── manager/
│ │ │ │ │ ├── DataStoreManager.kt
│ │ │ │ │ └── PreferenceRequest.kt
│ │ │ │ └── dev/
│ │ │ │ └── aaa1115910/
│ │ │ │ ├── bv/
│ │ │ │ │ ├── BVApp.kt
│ │ │ │ │ ├── activities/
│ │ │ │ │ │ └── LauncherActivity.kt
│ │ │ │ │ ├── component/
│ │ │ │ │ │ ├── BvPlayerPreview.kt
│ │ │ │ │ │ ├── DevelopingTip.kt
│ │ │ │ │ │ ├── FpsMonitor.kt
│ │ │ │ │ │ ├── QrImage.kt
│ │ │ │ │ │ └── settings/
│ │ │ │ │ │ └── UpdateDialog.kt
│ │ │ │ │ ├── dao/
│ │ │ │ │ │ ├── AppDatabase.kt
│ │ │ │ │ │ ├── SearchHistoryDao.kt
│ │ │ │ │ │ └── UserDao.kt
│ │ │ │ │ ├── entity/
│ │ │ │ │ │ ├── AuthData.kt
│ │ │ │ │ │ ├── BvScheme.kt
│ │ │ │ │ │ ├── InterfaceMode.kt
│ │ │ │ │ │ ├── NavSwitchMode.kt
│ │ │ │ │ │ ├── PlayerType.kt
│ │ │ │ │ │ ├── ThemeType.kt
│ │ │ │ │ │ ├── carddata/
│ │ │ │ │ │ │ ├── SeasonCardData.kt
│ │ │ │ │ │ │ └── VideoCardData.kt
│ │ │ │ │ │ ├── db/
│ │ │ │ │ │ │ ├── SearchHistoryDB.kt
│ │ │ │ │ │ │ └── UserDB.kt
│ │ │ │ │ │ └── proxy/
│ │ │ │ │ │ └── ProxyArea.kt
│ │ │ │ │ ├── network/
│ │ │ │ │ │ ├── GithubApi.kt
│ │ │ │ │ │ ├── HttpServer.kt
│ │ │ │ │ │ ├── VlcLibsApi.kt
│ │ │ │ │ │ └── entity/
│ │ │ │ │ │ └── GithubRelease.kt
│ │ │ │ │ ├── player/
│ │ │ │ │ │ └── entity/
│ │ │ │ │ │ ├── DefaultSubtitle.kt
│ │ │ │ │ │ ├── NextVideoStrategy.kt
│ │ │ │ │ │ ├── PlayerDefaultStartPosition.kt
│ │ │ │ │ │ └── PlayerLoadNextAction.kt
│ │ │ │ │ ├── repository/
│ │ │ │ │ │ ├── UserRepository.kt
│ │ │ │ │ │ └── VideoInfoRepository.kt
│ │ │ │ │ ├── ui/
│ │ │ │ │ │ └── theme/
│ │ │ │ │ │ ├── Theme.kt
│ │ │ │ │ │ └── Typography.kt
│ │ │ │ │ ├── util/
│ │ │ │ │ │ ├── AbiUtil.kt
│ │ │ │ │ │ ├── BlacklistUtil.kt
│ │ │ │ │ │ ├── CodecUtil.kt
│ │ │ │ │ │ ├── CoilConfig.kt
│ │ │ │ │ │ ├── DanmakuRateLimiter.kt
│ │ │ │ │ │ ├── DeviceUtil.kt
│ │ │ │ │ │ ├── EnumExtends.kt
│ │ │ │ │ │ ├── Extends.kt
│ │ │ │ │ │ ├── LiveStreamUrlFetcher.kt
│ │ │ │ │ │ ├── LogCatcherUtil.kt
│ │ │ │ │ │ ├── ModifierExtends.kt
│ │ │ │ │ │ ├── NetworkUtil.kt
│ │ │ │ │ │ ├── NotYetImplemented.kt
│ │ │ │ │ │ ├── PartitionUtil.kt
│ │ │ │ │ │ ├── PgcIndexParamExtends.kt
│ │ │ │ │ │ ├── PgcTypeExtends.kt
│ │ │ │ │ │ ├── Prefs.kt
│ │ │ │ │ │ ├── UgcTypeExtends.kt
│ │ │ │ │ │ ├── UgcTypeV2Extends.kt
│ │ │ │ │ │ └── calculateWindowSizeClassInPreview.kt
│ │ │ │ │ └── viewmodel/
│ │ │ │ │ ├── CommentViewModel.kt
│ │ │ │ │ ├── DynamicDetailViewModel.kt
│ │ │ │ │ ├── SeasonViewModel.kt
│ │ │ │ │ ├── TagViewModel.kt
│ │ │ │ │ ├── UserSwitchViewModel.kt
│ │ │ │ │ ├── UserViewModel.kt
│ │ │ │ │ ├── VideoPlayerV3ViewModel.kt
│ │ │ │ │ ├── home/
│ │ │ │ │ │ ├── DynamicViewModel.kt
│ │ │ │ │ │ ├── PopularViewModel.kt
│ │ │ │ │ │ └── RecommendViewModel.kt
│ │ │ │ │ ├── index/
│ │ │ │ │ │ └── PgcIndexViewModel.kt
│ │ │ │ │ ├── live/
│ │ │ │ │ │ └── LiveViewModel.kt
│ │ │ │ │ ├── login/
│ │ │ │ │ │ ├── AppQrLoginViewModel.kt
│ │ │ │ │ │ └── SmsLoginViewModel.kt
│ │ │ │ │ ├── pgc/
│ │ │ │ │ │ ├── PgcAnimeViewModel.kt
│ │ │ │ │ │ ├── PgcDocumentaryViewModel.kt
│ │ │ │ │ │ ├── PgcGuoChuangViewModel.kt
│ │ │ │ │ │ ├── PgcMovieViewModel.kt
│ │ │ │ │ │ ├── PgcTvViewModel.kt
│ │ │ │ │ │ ├── PgcVarietyViewModel.kt
│ │ │ │ │ │ └── PgcViewModel.kt
│ │ │ │ │ ├── search/
│ │ │ │ │ │ ├── SearchInputViewModel.kt
│ │ │ │ │ │ └── SearchResultViewModel.kt
│ │ │ │ │ ├── ugc/
│ │ │ │ │ │ ├── UgcAiViewModel.kt
│ │ │ │ │ │ ├── UgcAnimalViewModel.kt
│ │ │ │ │ │ ├── UgcCarViewModel.kt
│ │ │ │ │ │ ├── UgcCinephileViewModel.kt
│ │ │ │ │ │ ├── UgcDanceViewModel.kt
│ │ │ │ │ │ ├── UgcDougaViewModel.kt
│ │ │ │ │ │ ├── UgcEmotionViewModel.kt
│ │ │ │ │ │ ├── UgcEntViewModel.kt
│ │ │ │ │ │ ├── UgcFashionViewModel.kt
│ │ │ │ │ │ ├── UgcFoodViewModel.kt
│ │ │ │ │ │ ├── UgcGameViewModel.kt
│ │ │ │ │ │ ├── UgcGymViewModel.kt
│ │ │ │ │ │ ├── UgcHandmakeViewModel.kt
│ │ │ │ │ │ ├── UgcHealthViewModel.kt
│ │ │ │ │ │ ├── UgcHomeViewModel.kt
│ │ │ │ │ │ ├── UgcInformationViewModel.kt
│ │ │ │ │ │ ├── UgcKichikuViewModel.kt
│ │ │ │ │ │ ├── UgcKnowledgeViewModel.kt
│ │ │ │ │ │ ├── UgcLifeExperienceViewModel.kt
│ │ │ │ │ │ ├── UgcLifeJoyViewModel.kt
│ │ │ │ │ │ ├── UgcMusicViewModel.kt
│ │ │ │ │ │ ├── UgcMysticismViewModel.kt
│ │ │ │ │ │ ├── UgcOutdoorsViewModel.kt
│ │ │ │ │ │ ├── UgcPaintingViewModel.kt
│ │ │ │ │ │ ├── UgcParentingViewModel.kt
│ │ │ │ │ │ ├── UgcRuralViewModel.kt
│ │ │ │ │ │ ├── UgcShortplayViewModel.kt
│ │ │ │ │ │ ├── UgcSportsViewModel.kt
│ │ │ │ │ │ ├── UgcTechViewModel.kt
│ │ │ │ │ │ ├── UgcTravelViewModel.kt
│ │ │ │ │ │ ├── UgcViewModel.kt
│ │ │ │ │ │ └── UgcVlogViewModel.kt
│ │ │ │ │ ├── user/
│ │ │ │ │ │ ├── FavoriteViewModel.kt
│ │ │ │ │ │ ├── FollowViewModel.kt
│ │ │ │ │ │ ├── FollowingSeasonViewModel.kt
│ │ │ │ │ │ ├── HistoryViewModel.kt
│ │ │ │ │ │ ├── ToViewViewModel.kt
│ │ │ │ │ │ └── UserSpaceViewModel.kt
│ │ │ │ │ └── video/
│ │ │ │ │ └── VideoDetailViewModel.kt
│ │ │ │ └── m3qrcode/
│ │ │ │ ├── DampedString.kt
│ │ │ │ ├── EmphasizedInterpolator.kt
│ │ │ │ ├── EntryAnimationStyle.kt
│ │ │ │ ├── MaterialShapeQr.kt
│ │ │ │ ├── MaterialShapeQrErrorCorrectionLevel.kt
│ │ │ │ ├── MaterialShapeQrState.kt
│ │ │ │ └── MaterialShapeRenderer.kt
│ │ │ ├── proto/
│ │ │ │ └── blacklist.proto
│ │ │ └── res/
│ │ │ ├── drawable/
│ │ │ │ ├── ic_banner_foreground.xml
│ │ │ │ ├── ic_banner_foreground_2.xml
│ │ │ │ ├── ic_danmaku_count.xml
│ │ │ │ ├── ic_gamer_ani.xml
│ │ │ │ ├── ic_launcher_foreground.xml
│ │ │ │ ├── ic_launcher_foreground_2.xml
│ │ │ │ ├── ic_play_count.xml
│ │ │ │ ├── ic_up.xml
│ │ │ │ ├── qrcode_hor_bar_s2_capsule.xml
│ │ │ │ ├── qrcode_hor_bar_s2_half_capsule.xml
│ │ │ │ ├── qrcode_hor_bar_s3_capsule.xml
│ │ │ │ ├── qrcode_hor_bar_s3_half_capsule.xml
│ │ │ │ ├── qrcode_square_s1_circle.xml
│ │ │ │ ├── qrcode_square_s1_drop.xml
│ │ │ │ ├── qrcode_square_s1_semi_circle.xml
│ │ │ │ ├── qrcode_square_s1_square.xml
│ │ │ │ ├── qrcode_square_s2_circle.xml
│ │ │ │ ├── qrcode_square_s2_clover.xml
│ │ │ │ ├── qrcode_square_s2_hexagonal.xml
│ │ │ │ ├── qrcode_square_s2_meteroid.xml
│ │ │ │ ├── qrcode_square_s2_wiggle_star.xml
│ │ │ │ ├── qrcode_square_s3_circle.xml
│ │ │ │ ├── qrcode_square_s3_clover.xml
│ │ │ │ ├── qrcode_square_s3_hexagonal.xml
│ │ │ │ ├── qrcode_square_s3_meteroid.xml
│ │ │ │ ├── qrcode_square_s3_wiggle_star.xml
│ │ │ │ ├── qrcode_square_s7_ring.xml
│ │ │ │ ├── qrcode_ver_bar_s2_capsule.xml
│ │ │ │ └── qrcode_ver_bar_s3_capsule.xml
│ │ │ ├── mipmap-anydpi-v26/
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── raw/
│ │ │ │ ├── ic_playing.json
│ │ │ │ └── lottie_qrcode_background.json
│ │ │ ├── values/
│ │ │ │ ├── arrays.xml
│ │ │ │ ├── colors.xml
│ │ │ │ ├── strings.xml
│ │ │ │ └── themes.xml
│ │ │ └── xml/
│ │ │ ├── network_security_config.xml
│ │ │ └── provider_paths.xml
│ │ ├── r8Test/
│ │ │ ├── AndroidManifest.xml
│ │ │ └── res/
│ │ │ └── values/
│ │ │ └── strings.xml
│ │ └── test/
│ │ └── kotlin/
│ │ ├── android/
│ │ │ └── util/
│ │ │ └── Log.kt
│ │ └── dev/
│ │ └── aaa1115910/
│ │ └── bv/
│ │ └── network/
│ │ └── GithubApiTest.kt
│ ├── src/
│ │ └── main/
│ │ └── AndroidManifest.xml
│ └── tv/
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── consumer-rules.pro
│ ├── proguard-rules.pro
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ ├── kotlin/
│ │ └── dev/
│ │ └── aaa1115910/
│ │ └── bv/
│ │ └── tv/
│ │ ├── activities/
│ │ │ ├── MainActivity.kt
│ │ │ ├── pgc/
│ │ │ │ ├── PgcIndexActivity.kt
│ │ │ │ └── anime/
│ │ │ │ └── AnimeTimelineActivity.kt
│ │ │ ├── search/
│ │ │ │ ├── SearchInputActivity.kt
│ │ │ │ └── SearchResultActivity.kt
│ │ │ ├── settings/
│ │ │ │ ├── LogsActivity.kt
│ │ │ │ ├── MediaCodecActivity.kt
│ │ │ │ ├── SettingsActivity.kt
│ │ │ │ └── SpeedTestActivity.kt
│ │ │ ├── user/
│ │ │ │ ├── FavoriteActivity.kt
│ │ │ │ ├── FollowActivity.kt
│ │ │ │ ├── FollowingSeasonActivity.kt
│ │ │ │ ├── HistoryActivity.kt
│ │ │ │ ├── LoginActivity.kt
│ │ │ │ ├── ToViewActivity.kt
│ │ │ │ ├── UserInfoActivity.kt
│ │ │ │ ├── UserLockSettingsActivity.kt
│ │ │ │ └── UserSwitchActivity.kt
│ │ │ └── video/
│ │ │ ├── RemoteControllerPanelDemoActivity.kt
│ │ │ ├── SeasonInfoActivity.kt
│ │ │ ├── TagActivity.kt
│ │ │ ├── UpInfoActivity.kt
│ │ │ ├── VideoInfoActivity.kt
│ │ │ └── VideoPlayerV3Activity.kt
│ │ ├── component/
│ │ │ ├── Carousel.kt
│ │ │ ├── CommentItem.kt
│ │ │ ├── CommentPanel.kt
│ │ │ ├── DescriptionPanel.kt
│ │ │ ├── FullscreenImageViewer.kt
│ │ │ ├── GeetestTvVerifyDialog.kt
│ │ │ ├── LibVLCDownloaderDialog.kt
│ │ │ ├── LoadingTip.kt
│ │ │ ├── RemoteControlPanelDemo.kt
│ │ │ ├── SubCommentItem.kt
│ │ │ ├── SubCommentPanel.kt
│ │ │ ├── TopNav.kt
│ │ │ ├── TvAlertDialog.kt
│ │ │ ├── UpIcon.kt
│ │ │ ├── UserPanel.kt
│ │ │ ├── buttons/
│ │ │ │ ├── CoinButton.kt
│ │ │ │ ├── FavoriteButton.kt
│ │ │ │ ├── LikeButton.kt
│ │ │ │ ├── SeasonInfoButtons.kt
│ │ │ │ └── ToViewButton.kt
│ │ │ ├── live/
│ │ │ │ └── LiveRoomCard.kt
│ │ │ ├── pgc/
│ │ │ │ └── IndexFilter.kt
│ │ │ ├── search/
│ │ │ │ ├── SearchKeyword.kt
│ │ │ │ └── SoftKeyboard.kt
│ │ │ ├── settings/
│ │ │ │ ├── SettingListItem.kt
│ │ │ │ ├── SettingNumberListItem.kt
│ │ │ │ ├── SettingSwitchListItem.kt
│ │ │ │ ├── SettingsMenuSelectItem.kt
│ │ │ │ └── UpdateDialog.kt
│ │ │ └── videocard/
│ │ │ ├── LargeVideoCard.kt
│ │ │ ├── SeasonCard.kt
│ │ │ ├── SmallVideoCard.kt
│ │ │ ├── TabbedVideosPanel.kt
│ │ │ └── VideosRow.kt
│ │ ├── manager/
│ │ │ ├── FollowStateManager.kt
│ │ │ ├── PlayedAidsCache.kt
│ │ │ └── VideoUserActionManager.kt
│ │ ├── screens/
│ │ │ ├── MainScreen.kt
│ │ │ ├── RegionBlockScreen.kt
│ │ │ ├── SeasonInfoScreen.kt
│ │ │ ├── TagScreen.kt
│ │ │ ├── VideoInfoScreen.kt
│ │ │ ├── VideoPlayerV3Screen.kt
│ │ │ ├── login/
│ │ │ │ ├── AppQRLoginContent.kt
│ │ │ │ ├── LoginScreen.kt
│ │ │ │ └── SmsLoginContent.kt
│ │ │ ├── main/
│ │ │ │ ├── DrawerContent.kt
│ │ │ │ ├── HomeContent.kt
│ │ │ │ ├── LiveContent.kt
│ │ │ │ ├── PgcContent.kt
│ │ │ │ ├── UgcContent.kt
│ │ │ │ ├── home/
│ │ │ │ │ ├── DynamicsScreen.kt
│ │ │ │ │ ├── PopularScreen.kt
│ │ │ │ │ └── RecommendScreen.kt
│ │ │ │ ├── pgc/
│ │ │ │ │ ├── AnimeContent.kt
│ │ │ │ │ ├── DocumentaryContent.kt
│ │ │ │ │ ├── GuoChuangContent.kt
│ │ │ │ │ ├── MovieContent.kt
│ │ │ │ │ ├── PgcCommon.kt
│ │ │ │ │ ├── PgcIndexScreen.kt
│ │ │ │ │ ├── TvContent.kt
│ │ │ │ │ ├── VarietyContent.kt
│ │ │ │ │ └── anime/
│ │ │ │ │ └── AnimeTimelineScreen.kt
│ │ │ │ └── ugc/
│ │ │ │ ├── UgcChildRegionButtons.kt
│ │ │ │ ├── UgcCommon.kt
│ │ │ │ ├── UgcContentFactory.kt
│ │ │ │ └── UgcStateManager.kt
│ │ │ ├── search/
│ │ │ │ ├── SearchInputScreen.kt
│ │ │ │ ├── SearchResultFilter.kt
│ │ │ │ └── SearchResultScreen.kt
│ │ │ ├── settings/
│ │ │ │ ├── LogsScreen.kt
│ │ │ │ ├── MediaCodecScreen.kt
│ │ │ │ ├── SettingsScreen.kt
│ │ │ │ ├── SpeedTestScreen.kt
│ │ │ │ └── content/
│ │ │ │ ├── AboutSetting.kt
│ │ │ │ ├── InfoSetting.kt
│ │ │ │ ├── NetworkSetting.kt
│ │ │ │ ├── OtherSetting.kt
│ │ │ │ ├── PlayerSetting.kt
│ │ │ │ ├── PlayerTypeSetting.kt
│ │ │ │ ├── StorageSetting.kt
│ │ │ │ └── UISetting.kt
│ │ │ └── user/
│ │ │ ├── FavoriteScreen.kt
│ │ │ ├── FollowScreen.kt
│ │ │ ├── FollowingSeasonFilter.kt
│ │ │ ├── FollowingSeasonScreen.kt
│ │ │ ├── HistoryScreen.kt
│ │ │ ├── ToViewScreen.kt
│ │ │ ├── UpInfoScreen.kt
│ │ │ ├── UserInfoScreen.kt
│ │ │ ├── UserSwitchScreen.kt
│ │ │ └── lock/
│ │ │ ├── UnlockSwitchUserContent.kt
│ │ │ ├── UnlockUserScreen.kt
│ │ │ └── UserLockSettingsScreen.kt
│ │ └── util/
│ │ ├── NavItemsExtensions.kt
│ │ ├── PlayerActivityUtil.kt
│ │ ├── ProvideListBringIntoViewSpec.kt
│ │ └── TvLazyListFocusRestorer.kt
│ └── res/
│ └── values/
│ ├── dimens.xml
│ ├── strings.xml
│ └── themes.xml
├── bili-api/
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── example-response/
│ │ └── live-event/
│ │ ├── COMBO_SEND.json5
│ │ ├── DANMU_MSG.json5
│ │ ├── ENTRY_EFFECT.json5
│ │ ├── GUARD_BUY.json5
│ │ ├── HOT_RANK_CHANGED.json5
│ │ ├── HOT_RANK_CHANGED_V2.json5
│ │ ├── HOT_RANK_SETTLEMENT.json5
│ │ ├── HOT_RANK_SETTLEMENT_V2.json5
│ │ ├── HOT_ROOM_NOTIFY.json5
│ │ ├── INTERACT_WORD.json5
│ │ ├── LIKE_INFO_V3_CLICK.json5
│ │ ├── LIKE_INFO_V3_UPDATE.json5
│ │ ├── LIVE_INTERACTIVE_GAME.json5
│ │ ├── LIVE_MULTI_VIEW_CHANGE.json5
│ │ ├── NOTICE_MSG.json5
│ │ ├── ONLINE_RANK_COUNT.json5
│ │ ├── ONLINE_RANK_TOP3.json5
│ │ ├── ONLINE_RANK_V2.json5
│ │ ├── PREPARING.json5
│ │ ├── ROOM_REAL_TIME_MESSAGE_UPDATE.json
│ │ ├── SEND_GIFT.json
│ │ ├── STOP_LIVE_ROOM_LIST.json5
│ │ ├── SUPER_CHAT_ENTRANCE.json5
│ │ ├── SUPER_CHAT_MESSAGE.json5
│ │ ├── SUPER_CHAT_MESSAGE_JPN.json5
│ │ ├── USER_TOAST_MSG.json5
│ │ ├── WATCHED_CHANGE.json5
│ │ └── WIDGET_BANNER.json5
│ ├── grpc/
│ │ ├── .gitignore
│ │ ├── build.gradle.kts
│ │ └── proto/
│ │ ├── bilibili/
│ │ │ ├── account/
│ │ │ │ └── fission/
│ │ │ │ └── v1/
│ │ │ │ └── fission.proto
│ │ │ ├── ad/
│ │ │ │ └── v1/
│ │ │ │ └── ad.proto
│ │ │ ├── api/
│ │ │ │ ├── player/
│ │ │ │ │ └── v1/
│ │ │ │ │ └── player.proto
│ │ │ │ ├── probe/
│ │ │ │ │ └── v1/
│ │ │ │ │ └── probe.proto
│ │ │ │ └── ticket/
│ │ │ │ └── v1/
│ │ │ │ └── ticket.proto
│ │ │ ├── app/
│ │ │ │ ├── archive/
│ │ │ │ │ ├── middleware/
│ │ │ │ │ │ └── v1/
│ │ │ │ │ │ └── preload.proto
│ │ │ │ │ └── v1/
│ │ │ │ │ └── archive.proto
│ │ │ │ ├── card/
│ │ │ │ │ └── v1/
│ │ │ │ │ ├── ad.proto
│ │ │ │ │ ├── card.proto
│ │ │ │ │ ├── common.proto
│ │ │ │ │ ├── double.proto
│ │ │ │ │ └── single.proto
│ │ │ │ ├── click/
│ │ │ │ │ └── v1/
│ │ │ │ │ └── heartbeat.proto
│ │ │ │ ├── distribution/
│ │ │ │ │ ├── setting/
│ │ │ │ │ │ ├── download.proto
│ │ │ │ │ │ ├── dynamic.proto
│ │ │ │ │ │ ├── experimental.proto
│ │ │ │ │ │ ├── internaldevice.proto
│ │ │ │ │ │ ├── night.proto
│ │ │ │ │ │ ├── other.proto
│ │ │ │ │ │ ├── pegasus.proto
│ │ │ │ │ │ ├── play.proto
│ │ │ │ │ │ ├── privacy.proto
│ │ │ │ │ │ └── search.proto
│ │ │ │ │ └── v1/
│ │ │ │ │ └── distribution.proto
│ │ │ │ ├── dynamic/
│ │ │ │ │ ├── common/
│ │ │ │ │ │ └── dynamic.proto
│ │ │ │ │ ├── v1/
│ │ │ │ │ │ └── dynamic.proto
│ │ │ │ │ └── v2/
│ │ │ │ │ ├── campus.proto
│ │ │ │ │ ├── dynamic.proto
│ │ │ │ │ └── opus.proto
│ │ │ │ ├── interfaces/
│ │ │ │ │ └── v1/
│ │ │ │ │ ├── history.proto
│ │ │ │ │ ├── media.proto
│ │ │ │ │ ├── search.proto
│ │ │ │ │ └── space.proto
│ │ │ │ ├── listener/
│ │ │ │ │ └── v1/
│ │ │ │ │ └── listener.proto
│ │ │ │ ├── playeronline/
│ │ │ │ │ └── v1/
│ │ │ │ │ └── playeronline.proto
│ │ │ │ ├── playerunite/
│ │ │ │ │ ├── pgcanymodel/
│ │ │ │ │ │ └── PGCAnyModel.proto
│ │ │ │ │ ├── ugcanymodel/
│ │ │ │ │ │ └── UGCAnyModel.proto
│ │ │ │ │ └── v1/
│ │ │ │ │ └── playerunite.proto
│ │ │ │ ├── playurl/
│ │ │ │ │ └── v1/
│ │ │ │ │ └── playurl.proto
│ │ │ │ ├── resource/
│ │ │ │ │ ├── privacy/
│ │ │ │ │ │ └── v1/
│ │ │ │ │ │ └── api.proto
│ │ │ │ │ └── v1/
│ │ │ │ │ └── module.proto
│ │ │ │ ├── search/
│ │ │ │ │ └── v2/
│ │ │ │ │ └── search.proto
│ │ │ │ ├── show/
│ │ │ │ │ ├── gateway/
│ │ │ │ │ │ └── v1/
│ │ │ │ │ │ └── service.proto
│ │ │ │ │ ├── mixture/
│ │ │ │ │ │ └── v1/
│ │ │ │ │ │ └── mixture.proto
│ │ │ │ │ ├── popular/
│ │ │ │ │ │ └── v1/
│ │ │ │ │ │ └── popular.proto
│ │ │ │ │ ├── rank/
│ │ │ │ │ │ └── v1/
│ │ │ │ │ │ └── rank.proto
│ │ │ │ │ └── region/
│ │ │ │ │ └── v1/
│ │ │ │ │ └── region.proto
│ │ │ │ ├── space/
│ │ │ │ │ └── v1/
│ │ │ │ │ └── space.proto
│ │ │ │ ├── splash/
│ │ │ │ │ └── v1/
│ │ │ │ │ └── splash.proto
│ │ │ │ ├── topic/
│ │ │ │ │ └── v1/
│ │ │ │ │ └── topic.proto
│ │ │ │ ├── view/
│ │ │ │ │ └── v1/
│ │ │ │ │ └── view.proto
│ │ │ │ ├── viewunite/
│ │ │ │ │ ├── common.proto
│ │ │ │ │ ├── pgcanymodel.proto
│ │ │ │ │ ├── ugcanymodel.proto
│ │ │ │ │ └── v1/
│ │ │ │ │ └── viewunite.proto
│ │ │ │ └── wall/
│ │ │ │ └── v1/
│ │ │ │ └── wall.proto
│ │ │ ├── broadcast/
│ │ │ │ ├── message/
│ │ │ │ │ ├── editor/
│ │ │ │ │ │ └── notify.proto
│ │ │ │ │ ├── esports/
│ │ │ │ │ │ └── notify.proto
│ │ │ │ │ ├── fission/
│ │ │ │ │ │ └── notify.proto
│ │ │ │ │ ├── im/
│ │ │ │ │ │ └── notify.proto
│ │ │ │ │ ├── main/
│ │ │ │ │ │ ├── dm.proto
│ │ │ │ │ │ ├── native.proto
│ │ │ │ │ │ ├── resource.proto
│ │ │ │ │ │ └── search.proto
│ │ │ │ │ ├── note/
│ │ │ │ │ │ └── sync.proto
│ │ │ │ │ ├── ogv/
│ │ │ │ │ │ ├── freya.proto
│ │ │ │ │ │ └── live.proto
│ │ │ │ │ ├── ticket/
│ │ │ │ │ │ └── activitygame.proto
│ │ │ │ │ └── tv/
│ │ │ │ │ └── proj.proto
│ │ │ │ ├── v1/
│ │ │ │ │ ├── broadcast.proto
│ │ │ │ │ ├── laser.proto
│ │ │ │ │ ├── mod.proto
│ │ │ │ │ ├── push.proto
│ │ │ │ │ ├── room.proto
│ │ │ │ │ └── test.proto
│ │ │ │ └── v2/
│ │ │ │ └── laser.proto
│ │ │ ├── cheese/
│ │ │ │ └── gateway/
│ │ │ │ └── player/
│ │ │ │ └── v1/
│ │ │ │ └── playurl.proto
│ │ │ ├── community/
│ │ │ │ └── service/
│ │ │ │ ├── dm/
│ │ │ │ │ └── v1/
│ │ │ │ │ └── dm.proto
│ │ │ │ └── govern/
│ │ │ │ └── v1/
│ │ │ │ └── govern.proto
│ │ │ ├── dagw/
│ │ │ │ └── component/
│ │ │ │ └── avatar/
│ │ │ │ ├── common/
│ │ │ │ │ └── common.proto
│ │ │ │ └── v1/
│ │ │ │ ├── avatar.proto
│ │ │ │ └── plugin.proto
│ │ │ ├── dynamic/
│ │ │ │ ├── common/
│ │ │ │ │ └── dynamic.proto
│ │ │ │ ├── gw/
│ │ │ │ │ └── gateway.proto
│ │ │ │ └── interfaces/
│ │ │ │ └── feed/
│ │ │ │ └── v1/
│ │ │ │ └── api.proto
│ │ │ ├── gaia/
│ │ │ │ └── gw/
│ │ │ │ └── gw_api.proto
│ │ │ ├── im/
│ │ │ │ ├── interfaces/
│ │ │ │ │ ├── inner-interface/
│ │ │ │ │ │ └── v1/
│ │ │ │ │ │ └── api.proto
│ │ │ │ │ └── v1/
│ │ │ │ │ └── im.proto
│ │ │ │ └── type/
│ │ │ │ └── im.proto
│ │ │ ├── live/
│ │ │ │ ├── app/
│ │ │ │ │ └── room/
│ │ │ │ │ └── v1/
│ │ │ │ │ └── room.proto
│ │ │ │ └── general/
│ │ │ │ └── interfaces/
│ │ │ │ └── v1/
│ │ │ │ └── interfaces.proto
│ │ │ ├── main/
│ │ │ │ ├── common/
│ │ │ │ │ └── arch/
│ │ │ │ │ └── doll/
│ │ │ │ │ └── v1/
│ │ │ │ │ └── doll.proto
│ │ │ │ └── community/
│ │ │ │ └── reply/
│ │ │ │ └── v1/
│ │ │ │ └── reply.proto
│ │ │ ├── metadata/
│ │ │ │ ├── device/
│ │ │ │ │ └── device.proto
│ │ │ │ ├── fawkes/
│ │ │ │ │ └── fawkes.proto
│ │ │ │ ├── locale/
│ │ │ │ │ └── locale.proto
│ │ │ │ ├── metadata.proto
│ │ │ │ ├── network/
│ │ │ │ │ └── network.proto
│ │ │ │ ├── parabox/
│ │ │ │ │ └── parabox.proto
│ │ │ │ └── restriction/
│ │ │ │ └── restriction.proto
│ │ │ ├── pagination/
│ │ │ │ └── pagination.proto
│ │ │ ├── pangu/
│ │ │ │ └── gallery/
│ │ │ │ └── v1/
│ │ │ │ └── gallery.proto
│ │ │ ├── pgc/
│ │ │ │ ├── gateway/
│ │ │ │ │ └── player/
│ │ │ │ │ ├── v1/
│ │ │ │ │ │ └── playurl.proto
│ │ │ │ │ └── v2/
│ │ │ │ │ └── playurl.proto
│ │ │ │ └── service/
│ │ │ │ └── premiere/
│ │ │ │ └── v1/
│ │ │ │ └── premiere.proto
│ │ │ ├── playershared/
│ │ │ │ └── playershared.proto
│ │ │ ├── polymer/
│ │ │ │ ├── app/
│ │ │ │ │ └── search/
│ │ │ │ │ └── v1/
│ │ │ │ │ └── search.proto
│ │ │ │ ├── community/
│ │ │ │ │ └── govern/
│ │ │ │ │ └── v1/
│ │ │ │ │ └── govern.proto
│ │ │ │ ├── contract/
│ │ │ │ │ └── v1/
│ │ │ │ │ └── contract.proto
│ │ │ │ ├── demo/
│ │ │ │ │ └── demo.proto
│ │ │ │ └── list/
│ │ │ │ └── v1/
│ │ │ │ └── list.proto
│ │ │ ├── relation/
│ │ │ │ └── interfaces/
│ │ │ │ └── api.proto
│ │ │ ├── render/
│ │ │ │ └── render.proto
│ │ │ ├── rpc/
│ │ │ │ └── status.proto
│ │ │ ├── tv/
│ │ │ │ └── interfaces/
│ │ │ │ └── dm/
│ │ │ │ └── v1/
│ │ │ │ └── dm.proto
│ │ │ ├── vega/
│ │ │ │ └── deneb/
│ │ │ │ └── v1/
│ │ │ │ └── deneb.proto
│ │ │ └── web/
│ │ │ ├── interfaces/
│ │ │ │ └── v1/
│ │ │ │ └── interfaces.proto
│ │ │ └── space/
│ │ │ └── v1/
│ │ │ └── space.proto
│ │ ├── common/
│ │ │ └── ErrorProto.proto
│ │ ├── datacenter/
│ │ │ └── hakase/
│ │ │ └── protobuf/
│ │ │ └── android_device_info.proto
│ │ └── pgc/
│ │ ├── biz/
│ │ │ └── room.proto
│ │ └── gateway/
│ │ └── vega/
│ │ └── v1/
│ │ └── vega.proto
│ └── src/
│ ├── main/
│ │ └── kotlin/
│ │ ├── com/
│ │ │ └── tfowl/
│ │ │ └── ktor/
│ │ │ └── client/
│ │ │ └── plugins/
│ │ │ └── JsoupPlugin.kt
│ │ └── dev/
│ │ └── aaa1115910/
│ │ └── biliapi/
│ │ ├── BiliApiConstants.kt
│ │ ├── entity/
│ │ │ ├── ApiType.kt
│ │ │ ├── CarouselData.kt
│ │ │ ├── CodeType.kt
│ │ │ ├── Favorite.kt
│ │ │ ├── Picture.kt
│ │ │ ├── PlayData.kt
│ │ │ ├── danmaku/
│ │ │ │ └── DanmakuMask.kt
│ │ │ ├── home/
│ │ │ │ └── RecommendData.kt
│ │ │ ├── live/
│ │ │ │ ├── LiveArea.kt
│ │ │ │ ├── LiveFollowing.kt
│ │ │ │ ├── LiveRecommend.kt
│ │ │ │ ├── LiveRoom.kt
│ │ │ │ └── LiveRoomPlayInfo.kt
│ │ │ ├── login/
│ │ │ │ ├── Captcha.kt
│ │ │ │ ├── QR.kt
│ │ │ │ └── Sms.kt
│ │ │ ├── pgc/
│ │ │ │ ├── PgcFeedData.kt
│ │ │ │ ├── PgcItem.kt
│ │ │ │ ├── PgcType.kt
│ │ │ │ └── index/
│ │ │ │ ├── IndexParams.kt
│ │ │ │ ├── PgcIndexCondition.kt
│ │ │ │ └── PgcIndexData.kt
│ │ │ ├── rank/
│ │ │ │ └── Popular.kt
│ │ │ ├── reply/
│ │ │ │ ├── Comment.kt
│ │ │ │ ├── CommentPage.kt
│ │ │ │ ├── CommentRepliesData.kt
│ │ │ │ └── CommentSort.kt
│ │ │ ├── search/
│ │ │ │ └── Hotword.kt
│ │ │ ├── season/
│ │ │ │ ├── FollowingSeasons.kt
│ │ │ │ ├── IndexResult.kt
│ │ │ │ └── Timeline.kt
│ │ │ ├── ugc/
│ │ │ │ ├── UgcItem.kt
│ │ │ │ ├── UgcType.kt
│ │ │ │ ├── UgcTypeV2.kt
│ │ │ │ └── region/
│ │ │ │ ├── UgcFeedData.kt
│ │ │ │ ├── UgcFeedPage.kt
│ │ │ │ ├── UgcRegionData.kt
│ │ │ │ ├── UgcRegionListData.kt
│ │ │ │ └── UgcRegionPage.kt
│ │ │ ├── user/
│ │ │ │ ├── Author.kt
│ │ │ │ ├── Dynamic.kt
│ │ │ │ ├── FollowedUser.kt
│ │ │ │ ├── History.kt
│ │ │ │ ├── Space.kt
│ │ │ │ └── ToView.kt
│ │ │ └── video/
│ │ │ ├── Dimension.kt
│ │ │ ├── Heartbeat.kt
│ │ │ ├── RelatedVideo.kt
│ │ │ ├── Subtitle.kt
│ │ │ ├── Tag.kt
│ │ │ ├── VideoDetail.kt
│ │ │ ├── VideoPage.kt
│ │ │ ├── VideoShot.kt
│ │ │ └── season/
│ │ │ ├── Episode.kt
│ │ │ ├── PgcSeason.kt
│ │ │ ├── SeasonDetail.kt
│ │ │ └── Section.kt
│ │ ├── grpc/
│ │ │ └── utils/
│ │ │ ├── Channel.kt
│ │ │ └── StatusExtends.kt
│ │ ├── http/
│ │ │ ├── BiliHttpApi.kt
│ │ │ ├── BiliHttpConstants.kt
│ │ │ ├── BiliHttpProxyApi.kt
│ │ │ ├── BiliLiveHttpApi.kt
│ │ │ ├── BiliPassportHttpApi.kt
│ │ │ ├── BiliPlusHttpApi.kt
│ │ │ ├── entity/
│ │ │ │ ├── BiliResponse.kt
│ │ │ │ ├── biliplus/
│ │ │ │ │ └── View.kt
│ │ │ │ ├── danmaku/
│ │ │ │ │ └── DanmakuResponse.kt
│ │ │ │ ├── dynamic/
│ │ │ │ │ ├── DynamicDetailResponse.kt
│ │ │ │ │ └── DynamicResponse.kt
│ │ │ │ ├── history/
│ │ │ │ │ └── HistoryData.kt
│ │ │ │ ├── home/
│ │ │ │ │ ├── RcmdIndexData.kt
│ │ │ │ │ └── RcmdTopData.kt
│ │ │ │ ├── index/
│ │ │ │ │ ├── IndexFilter.kt
│ │ │ │ │ ├── IndexFilterArea.kt
│ │ │ │ │ ├── IndexFilterProducerId.kt
│ │ │ │ │ ├── IndexFilterStyle.kt
│ │ │ │ │ ├── IndexOrder.kt
│ │ │ │ │ └── IndexResult.kt
│ │ │ │ ├── live/
│ │ │ │ │ ├── HistoryDanmaku.kt
│ │ │ │ │ ├── LiveDanmuInfoResponse.kt
│ │ │ │ │ ├── LiveEvent.kt
│ │ │ │ │ ├── LiveFrame.kt
│ │ │ │ │ └── LiveRoomPlayInfoResponse.kt
│ │ │ │ ├── login/
│ │ │ │ │ ├── Captcha.kt
│ │ │ │ │ ├── qr/
│ │ │ │ │ │ ├── AppQR.kt
│ │ │ │ │ │ └── WebQR.kt
│ │ │ │ │ └── sms/
│ │ │ │ │ ├── SendSmsResponse.kt
│ │ │ │ │ └── SmsLoginResponse.kt
│ │ │ │ ├── pgc/
│ │ │ │ │ ├── PgcFeed.kt
│ │ │ │ │ ├── PgcFeedV3.kt
│ │ │ │ │ └── PgcWebInitialStateData.kt
│ │ │ │ ├── proxy/
│ │ │ │ │ └── PlayUrl.kt
│ │ │ │ ├── region/
│ │ │ │ │ ├── RegionBanner.kt
│ │ │ │ │ ├── RegionDynamic.kt
│ │ │ │ │ ├── RegionDynamicList.kt
│ │ │ │ │ ├── RegionFeedRcmd.kt
│ │ │ │ │ └── RegionLocs.kt
│ │ │ │ ├── reply/
│ │ │ │ │ ├── Comment.kt
│ │ │ │ │ ├── CommentReplyData.kt
│ │ │ │ │ └── Layers.kt
│ │ │ │ ├── search/
│ │ │ │ │ ├── KeywordSuggest.kt
│ │ │ │ │ ├── SearchCost.kt
│ │ │ │ │ ├── SearchResult.kt
│ │ │ │ │ ├── SearchResultItem.kt
│ │ │ │ │ └── SearchSquare.kt
│ │ │ │ ├── season/
│ │ │ │ │ ├── AppSeasonData.kt
│ │ │ │ │ ├── Episode.kt
│ │ │ │ │ ├── Follow.kt
│ │ │ │ │ ├── SeasonSection.kt
│ │ │ │ │ ├── WebFollowingSeason.kt
│ │ │ │ │ └── WebSeasonData.kt
│ │ │ │ ├── subtitle/
│ │ │ │ │ └── Subtitle.kt
│ │ │ │ ├── toview/
│ │ │ │ │ └── ToViewData.kt
│ │ │ │ ├── user/
│ │ │ │ │ ├── Follow.kt
│ │ │ │ │ ├── LevelInfo.kt
│ │ │ │ │ ├── Nameplate.kt
│ │ │ │ │ ├── Official.kt
│ │ │ │ │ ├── Pendant.kt
│ │ │ │ │ ├── Profession.kt
│ │ │ │ │ ├── Relation.kt
│ │ │ │ │ ├── SpaceVideoData.kt
│ │ │ │ │ ├── Staff.kt
│ │ │ │ │ ├── UserCardInfoResponse.kt
│ │ │ │ │ ├── UserGarb.kt
│ │ │ │ │ ├── UserHonours.kt
│ │ │ │ │ ├── UserInfoResponse.kt
│ │ │ │ │ ├── UserSelfInfoResponse.kt
│ │ │ │ │ ├── Vip.kt
│ │ │ │ │ ├── favorite/
│ │ │ │ │ │ ├── CntInfo.kt
│ │ │ │ │ │ ├── FavoriteFolderInfo.kt
│ │ │ │ │ │ ├── FavoriteFolderInfoListData.kt
│ │ │ │ │ │ ├── FavoriteItem.kt
│ │ │ │ │ │ ├── Upper.kt
│ │ │ │ │ │ └── UserFavoriteFoldersData.kt
│ │ │ │ │ └── garb/
│ │ │ │ │ ├── CardBg.kt
│ │ │ │ │ ├── Equip.kt
│ │ │ │ │ └── Item.kt
│ │ │ │ ├── video/
│ │ │ │ │ ├── AddCoin.kt
│ │ │ │ │ ├── ArchiveRelation.kt
│ │ │ │ │ ├── GaiaVgateData.kt
│ │ │ │ │ ├── PlayUrlResponse.kt
│ │ │ │ │ ├── PopularVideosResponse.kt
│ │ │ │ │ ├── RelatedVideosResponse.kt
│ │ │ │ │ ├── SetVideoFavorite.kt
│ │ │ │ │ ├── Tag.kt
│ │ │ │ │ ├── Timeline.kt
│ │ │ │ │ ├── UgcSeason.kt
│ │ │ │ │ ├── VideoDetail.kt
│ │ │ │ │ ├── VideoInfo.kt
│ │ │ │ │ ├── VideoMoreInfo.kt
│ │ │ │ │ ├── VideoOnlineTotal.kt
│ │ │ │ │ └── VideoShot.kt
│ │ │ │ └── web/
│ │ │ │ ├── Hover.kt
│ │ │ │ └── Nav.kt
│ │ │ ├── plugins/
│ │ │ │ └── BiliUserAgent.kt
│ │ │ └── util/
│ │ │ ├── ApiSign.kt
│ │ │ ├── BiliAppConf.kt
│ │ │ ├── BiliDns.kt
│ │ │ ├── BiliWebConf.kt
│ │ │ ├── Brotli.kt
│ │ │ ├── Buvid.kt
│ │ │ ├── CommonEnumIntSerializer.kt
│ │ │ └── Zlib.kt
│ │ ├── repositories/
│ │ │ ├── AuthRepository.kt
│ │ │ ├── BiliApiModule.kt
│ │ │ ├── ChannelRepository.kt
│ │ │ ├── CoinRepository.kt
│ │ │ ├── CommentRepository.kt
│ │ │ ├── FavoriteRepository.kt
│ │ │ ├── HistoryRepository.kt
│ │ │ ├── LikeRepository.kt
│ │ │ ├── LiveRepository.kt
│ │ │ ├── LoginRepository.kt
│ │ │ ├── PgcRepository.kt
│ │ │ ├── RecommendVideoRepository.kt
│ │ │ ├── SearchRepository.kt
│ │ │ ├── SeasonRepository.kt
│ │ │ ├── ToViewRepository.kt
│ │ │ ├── UgcRepository.kt
│ │ │ ├── UserRepository.kt
│ │ │ ├── VideoDetailRepository.kt
│ │ │ └── VideoPlayRepository.kt
│ │ ├── util/
│ │ │ ├── AvBvConverter.kt
│ │ │ ├── Extends.kt
│ │ │ └── UrlUtil.kt
│ │ └── websocket/
│ │ └── LiveDataWebSocket.kt
│ └── test/
│ ├── kotlin/
│ │ └── dev/
│ │ └── aaa1115910/
│ │ └── biliapi/
│ │ ├── BvLoginRepositoryTest.kt
│ │ ├── entity/
│ │ │ └── DanmakuMaskTest.kt
│ │ ├── http/
│ │ │ ├── BiliHttpApiTest.kt
│ │ │ ├── BiliLiveHttpApiTest.kt
│ │ │ ├── BiliPassportHttpApiTest.kt
│ │ │ └── BiliPlusHttpApiTest.kt
│ │ ├── repositories/
│ │ │ ├── CommentRepositoryTest.kt
│ │ │ ├── FavoriteRepositoryTest.kt
│ │ │ ├── HistoryRepositoryTest.kt
│ │ │ ├── PgcRepositoryTest.kt
│ │ │ ├── RecommendVideoRepositoryTest.kt
│ │ │ ├── SearchRepositoryTest.kt
│ │ │ ├── SeasonRepositoryTest.kt
│ │ │ ├── UgcRepositoryTest.kt
│ │ │ ├── UserRepositoryTest.kt
│ │ │ ├── VideoDetailRepositoryTest.kt
│ │ │ └── VideoPlayRepositoryTest.kt
│ │ └── websocket/
│ │ └── LiveDataWebSocketTest.kt
│ └── resources/
│ ├── 3540266_25_2.exp.mobmask
│ └── 3540266_25_2.exp.webmask
├── bili-subtitle/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ └── kotlin/
│ │ └── dev/
│ │ └── aaa1115910/
│ │ └── bilisubtitle/
│ │ ├── SubtitleEncoder.kt
│ │ ├── SubtitleParser.kt
│ │ └── entity/
│ │ ├── BiliSubtitle.kt
│ │ ├── SrtSubtitle.kt
│ │ ├── SubtitleItem.kt
│ │ └── Timestamp.kt
│ └── test/
│ ├── kotlin/
│ │ └── dev/
│ │ └── aaa1115910/
│ │ └── bilisubtitle/
│ │ ├── SubtitleEncoderTest.kt
│ │ ├── SubtitleParserTest.kt
│ │ └── entity/
│ │ └── TimestampTest.kt
│ └── resources/
│ ├── example.bcc
│ └── example.srt
├── build.gradle.kts
├── doc/
│ └── 弹幕/
│ ├── calc_danmaku_averages.js
│ ├── 弹幕code review 报告.md
│ ├── 弹幕库优化.md
│ ├── 弹幕重构需求.md
│ └── 重构后.txt
├── gradle/
│ ├── androidx.versions.toml
│ ├── gradle.versions.toml
│ ├── libs.versions.toml
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── player/
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── core/
│ │ ├── .gitignore
│ │ ├── build.gradle.kts
│ │ ├── consumer-rules.pro
│ │ ├── proguard-rules.pro
│ │ └── src/
│ │ └── main/
│ │ ├── AndroidManifest.xml
│ │ ├── assets/
│ │ │ └── GlobalSign ECC Root CA R5.crt
│ │ └── kotlin/
│ │ └── dev/
│ │ └── aaa1115910/
│ │ └── bv/
│ │ └── player/
│ │ ├── AbstractVideoPlayer.kt
│ │ ├── BvVideoPlayer.kt
│ │ ├── OkHttpUtil.kt
│ │ ├── VideoPlayerListener.kt
│ │ ├── VideoPlayerOptions.kt
│ │ ├── factory/
│ │ │ └── PlayerFactory.kt
│ │ └── impl/
│ │ └── exo/
│ │ ├── ExoMediaPlayer.kt
│ │ └── ExoPlayerFactory.kt
│ ├── mobile/
│ │ ├── .gitignore
│ │ ├── build.gradle.kts
│ │ ├── consumer-rules.pro
│ │ ├── proguard-rules.pro
│ │ └── src/
│ │ └── main/
│ │ ├── AndroidManifest.xml
│ │ └── kotlin/
│ │ └── dev/
│ │ └── aaa1115910/
│ │ └── bv/
│ │ └── player/
│ │ └── mobile/
│ │ ├── BvPlayer.kt
│ │ ├── MaterialDarkTheme.kt
│ │ ├── Media3VideoPlayer.kt
│ │ ├── NoRippleClickable.kt
│ │ ├── SeekBar.kt
│ │ └── controller/
│ │ ├── BvPlayerController.kt
│ │ ├── FullscreenControllers.kt
│ │ ├── MiniControllers.kt
│ │ ├── Tips.kt
│ │ └── menu/
│ │ ├── DanmakuMenu.kt
│ │ ├── DashMenu.kt
│ │ ├── MoreMenu.kt
│ │ ├── SpeedMenu.kt
│ │ └── VideoListMenu.kt
│ ├── shared/
│ │ ├── .gitignore
│ │ ├── build.gradle.kts
│ │ ├── consumer-rules.pro
│ │ ├── proguard-rules.pro
│ │ └── src/
│ │ └── main/
│ │ ├── AndroidManifest.xml
│ │ ├── kotlin/
│ │ │ └── dev/
│ │ │ └── aaa1115910/
│ │ │ └── bv/
│ │ │ └── player/
│ │ │ ├── danmaku/
│ │ │ │ ├── CacheManager.kt
│ │ │ │ ├── DanmakuConfig.kt
│ │ │ │ ├── DanmakuEngine.kt
│ │ │ │ ├── DanmakuLogStats.kt
│ │ │ │ ├── DanmakuPlayer.kt
│ │ │ │ ├── DanmakuTimer.kt
│ │ │ │ ├── DanmakuView.kt
│ │ │ │ └── model/
│ │ │ │ ├── Danmaku.kt
│ │ │ │ ├── DanmakuItem.kt
│ │ │ │ ├── DanmakuKind.kt
│ │ │ │ └── RenderSnapshot.kt
│ │ │ ├── entity/
│ │ │ │ ├── Audio.kt
│ │ │ │ ├── ControllerButtonConfig.kt
│ │ │ │ ├── DanmakuSize.kt
│ │ │ │ ├── DanmakuTransparency.kt
│ │ │ │ ├── DanmakuType.kt
│ │ │ │ ├── DefaultStartPosition.kt
│ │ │ │ ├── LiveCodec.kt
│ │ │ │ ├── PlayMode.kt
│ │ │ │ ├── PortraitVideoFixMode.kt
│ │ │ │ ├── RequestState.kt
│ │ │ │ ├── Resolution.kt
│ │ │ │ ├── VideoAspectRatio.kt
│ │ │ │ ├── VideoCodec.kt
│ │ │ │ ├── VideoListItem.kt
│ │ │ │ ├── VideoPlayerClosedCaptionMenuItem.kt
│ │ │ │ ├── VideoPlayerDanmakuMenuItem.kt
│ │ │ │ ├── VideoPlayerData.kt
│ │ │ │ ├── VideoPlayerMenuNavItem.kt
│ │ │ │ ├── VideoPlayerOthersMenuItem.kt
│ │ │ │ ├── VideoPlayerPictureMenuItem.kt
│ │ │ │ └── VideoRotation.kt
│ │ │ ├── seekbar/
│ │ │ │ ├── SeekBar.kt
│ │ │ │ ├── SeekBarThumb.kt
│ │ │ │ └── SeekMoveState.kt
│ │ │ └── util/
│ │ │ ├── DanmakuMaskFinder.kt
│ │ │ ├── DanmakuMaskModifiers.kt
│ │ │ └── VideoShotExtends.kt
│ │ └── res/
│ │ ├── drawable/
│ │ │ ├── ic_danmaku_hide.xml
│ │ │ ├── ic_danmaku_on.xml
│ │ │ ├── ic_play_mode_custom.xml
│ │ │ ├── ic_play_mode_list_order.xml
│ │ │ ├── ic_play_mode_list_order_reverse.xml
│ │ │ ├── ic_play_mode_part_and_episode.xml
│ │ │ ├── ic_play_mode_part_and_episode_reverse.xml
│ │ │ ├── ic_play_mode_related_video.xml
│ │ │ ├── ic_play_mode_single.xml
│ │ │ ├── ic_play_mode_single_loop.xml
│ │ │ ├── ic_subtitle_off.xml
│ │ │ ├── ic_subtitle_on.xml
│ │ │ ├── next_play_fill.xml
│ │ │ ├── person.xml
│ │ │ └── person_following.xml
│ │ └── values/
│ │ └── strings.xml
│ └── tv/
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── consumer-rules.pro
│ ├── proguard-rules.pro
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ └── kotlin/
│ └── dev/
│ └── aaa1115910/
│ └── bv/
│ └── player/
│ └── tv/
│ ├── BvPlayer.kt
│ ├── SeekBar.kt
│ └── controller/
│ ├── BottomSubtitle.kt
│ ├── ControllerVideoInfo.kt
│ ├── LiveViewerCountTip.kt
│ ├── MenuController.kt
│ ├── OnlineViewerCountTip.kt
│ ├── PlayStateTips.kt
│ ├── SeekController.kt
│ ├── SkipTip.kt
│ ├── UserActionContent.kt
│ ├── VideoListController.kt
│ ├── VideoPlayerController.kt
│ ├── VideoShot.kt
│ └── playermenu/
│ ├── ClosedCaptionMenu.kt
│ ├── DanmakuMenu.kt
│ ├── MenuNav.kt
│ ├── OthersMenu.kt
│ ├── PictureMenu.kt
│ └── component/
│ ├── CheckBoxMenuList.kt
│ ├── MenuListItem.kt
│ ├── RadioMenuList.kt
│ └── StepLessMenuItem.kt
├── settings.gradle.kts
├── symbols/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ └── main/
│ └── kotlin/
│ └── dev/
│ └── aaa1115910/
│ └── symbols/
│ └── Symbols.kt
└── utils/
├── .gitignore
├── build.gradle.kts
├── consumer-rules.pro
├── proguard-rules.pro
└── src/
└── main/
├── AndroidManifest.xml
└── kotlin/
└── dev/
└── aaa1115910/
└── bv/
└── util/
├── DateExtends.kt
├── Debounce.kt
├── FirebaseUtil.kt
├── FocusRequesterExtends.kt
├── ImageExtends.kt
├── KLoggerExtends.kt
├── KeyEventExtends.kt
├── LongExtends.kt
├── SnapshotStateListExtends.kt
├── Timer.kt
├── ToastExtends.kt
├── createCustomInitialFocusRestorerModifiers.kt
└── ifElse.kt
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: Bug 报告
description: 创建 Bug 报告以帮助开发者改进
title: "以简单的一段字概括你所遇到的问题"
body:
- type: markdown
attributes:
value: |
## 反馈须知
- 请务必完整填写下面的内容,如果缺少必要的信息,将无法解决任何问题
- 一个 issue 请只反馈一个 bug 或功能建议,一次性反馈多个不同的问题或建议或将会被直接关闭
- 注意你的标题,以简单的一段字概括你所遇到的问题。不要使用无意义内容或全部复制粘贴
- 该项目不为任何旧版本提供维护支持,请务必确认已更新到最新版本
- 应用仅支持系统硬件解码,如遇到播放卡顿或无法播放,请先检查设备芯片性能以及编码支持情况
- type: textarea
id: description
validations:
required: true
attributes:
label: Bug 描述
description: 请简短地描述你遇到的问题
- type: textarea
id: steps
validations:
required: true
attributes:
label: 复现问题的步骤
render: plain text
description: 请提供复现问题的步骤,如果不能,请写明原因
placeholder: |
示例步骤:
1. 进入 '...'
2. 点击 '....'
3. 滚动到 '....'
4. 出现问题
- type: textarea
id: expected-behavior
validations:
required: true
attributes:
label: 预期行为
description: 简要描述你希望看到什么样的结果
- type: textarea
id: screenshots
attributes:
label: 截图
description: 如果可以,提交截图更有助于我们分析问题
- type: dropdown
id: app-version-confirm-use-latest
validations:
required: true
attributes:
label: 请确认已更新到如下所示的版本
description: |

options:
- '我正在使用旧版本'
- '已更新到当前最新 Alpha 版'
- type: input
id: app-version
validations:
required: true
attributes:
label: 当前版本号
placeholder: 0.0.1.r29.a6d7ecb.release (或使用缩写例如 r29)
- type: input
id: android-version
validations:
required: true
attributes:
label: Android 版本
placeholder: Android 13
- type: input
id: device-info
attributes:
label: 设备厂商及型号
placeholder: Sony - BRAVIA XR MASTER SERIES Z9K
- type: input
id: video
attributes:
label: 遇到问题的视频 avid 或 bvid
placeholder: av170001
- type: textarea
id: additional-logs
attributes:
label: 相关日志
description: |
你可以在 `设置` > `更多设置` > `查看日志` 中查看已保存的日志,扫码即可下载获取(在同一网络环境下)
在日志列表中可找到自动生成的崩溃日志,或可在功能遇到问题(例如加载失败)后手动创建日志文件
上传文件时请务必等待文件上传完成后再提交 issue
- type: textarea
id: additional-content
attributes:
label: 附加信息
description: 添加你认为有必要的信息,例如出现问题的相关视频等等
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yml
================================================
name: 功能需求
description: 为项目提供建议
title: "以简单的一段字概括你的建议"
body:
- type: markdown
attributes:
value: |
## 反馈须知
- 请务必完整填写下面的内容,如果缺少必要的信息,将无法解决任何问题
- 一个 issue 请只反馈一个 bug 或功能建议,一次性反馈多个不同的问题或建议或将会被直接关闭
- 注意你的标题,以简单的一段字概括你所遇到的问题。不要使用无意义内容或全部复制粘贴
- 提出建议也不一定会做,这不可能是万能的许愿机,如果自己确实想要,建议 fork 项目自己实现
- 该项目不为任何旧版本提供维护支持,请务必确认已更新到最新版本
- type: textarea
id: problem-description
validations:
required: true
attributes:
label: 问题描述
description: 描述你想要解决的问题或者功能的使用场景
placeholder: 请描述你遇到的问题或者链接到已存在的 issue
- type: textarea
id: solution-description
validations:
required: true
attributes:
label: 描述解决方案
description: 清晰简明地描述你所想要发生的事情,即解决方案
- type: textarea
id: alternatives
attributes:
label: 描述备选方案
description: 解决该问题的备选方案
- type: textarea
id: additional-info
attributes:
label: 附加信息
description: 添加你认为有必要的信息
================================================
FILE: .github/workflows/alpha.yml
================================================
name: Alpha Build
on:
push:
branches:
- develop
jobs:
build-alpha:
name: Build Alpha Apk
runs-on: macos-latest
if: github.repository == 'aaa1115910/bv'
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: develop
fetch-depth: 0
submodules: 'true'
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
java-version: '21'
distribution: 'temurin'
- name: Setup Gradle to generate and submit dependency graphs
uses: gradle/actions/setup-gradle@v3
with:
dependency-graph: generate-and-submit
- name: Write google-services.json
env:
DATA: ${{ secrets.GOOGLE_SERVICES_JSON }}
run: echo $DATA > app/google-services.json
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Add signing properties
env:
SIGNING_PROPERTIES: ${{ secrets.SIGNING_PROPERTIES }}
run: |
echo ${{ secrets.SIGNING_PROPERTIES }} > encoded_signing_properties
base64 -Dd -i encoded_signing_properties > signing.properties
- name: Add jks file
run: |
echo ${{ secrets.SIGN_KEY }} > ./encoded_key
base64 -Dd -i encoded_key > key.jks
- name: Build apk
run: ./gradlew assembleDefaultAlpha assembleDefaultDebug
- name: Read alpha apk output metadata
id: apk-meta-alpha
uses: juliangruber/read-file-action@v1
with:
path: app/build/outputs/apk/default/alpha/output-metadata.json
- name: Read alpha debug apk output metadata
id: apk-meta-alpha-debug
uses: juliangruber/read-file-action@v1
with:
path: app/build/outputs/apk/default/debug/output-metadata.json
- name: Parse apk infos
id: apk-infos
run: |
echo "alpha_info_version_code=${{ fromJson(steps.apk-meta-alpha.outputs.content).elements[0].versionCode }}" >> $GITHUB_ENV
echo "alpha_info_version_name=${{ fromJson(steps.apk-meta-alpha.outputs.content).elements[0].versionName }}" >> $GITHUB_ENV
echo "alpha_debug_info_version_code=${{ fromJson(steps.apk-meta-alpha-debug.outputs.content).elements[0].versionCode }}" >> $GITHUB_ENV
echo "alpha_debug_info_version_name=${{ fromJson(steps.apk-meta-alpha-debug.outputs.content).elements[0].versionName }}" >> $GITHUB_ENV
- name: Determine tag name
id: tag_name
run: echo "tag_name=alpha-r${{ env.alpha_info_version_code }}" >> $GITHUB_ENV
- name: Get changelog
id: changelog
run: |
{
echo "changelog<<EOF"
echo "$(git log --pretty=format:"- %s (%h)" ${{ github.event.before }}..${{ github.sha }})"
echo "EOF"
} >> "$GITHUB_ENV"
# upload artifacts alpha debug
- name: Archive alpha debug build artifacts
uses: actions/upload-artifact@v4
with:
name: Alpha debug build artifact
path: app/build/outputs/apk/default/debug/BV_${{ env.alpha_debug_info_version_code }}_${{ env.alpha_debug_info_version_name }}_default_universal.apk
# upload artifacts alpha
- name: Archive default alpha build mappings
uses: actions/upload-artifact@v4
with:
name: Alpha build mappings
path: app/build/outputs/mapping/defaultAlpha
- name: Archive alpha build artifacts
uses: actions/upload-artifact@v4
with:
name: Alpha build artifact
path: app/build/outputs/apk/default/alpha/BV_${{ env.alpha_info_version_code }}_${{ env.alpha_info_version_name }}_default_universal.apk
# zip mapping because softprops/action-gh-release can't upload folder
- name: Zip mapping
run: zip -rj mapping.zip app/build/outputs/mapping/defaultAlpha
# upload to github release
- name: Publish Pre-Release
uses: softprops/action-gh-release@v2
with:
files: |
app/build/outputs/apk/default/debug/BV_${{ env.alpha_debug_info_version_code }}_${{ env.alpha_debug_info_version_name }}_default_universal.apk
app/build/outputs/apk/default/alpha/BV_${{ env.alpha_info_version_code }}_${{ env.alpha_info_version_name }}_default_universal.apk
mapping.zip
tag_name: ${{ env.tag_name }}
name: ${{ env.alpha_info_version_name }}
prerelease: true
body: ${{ env.changelog }}
target_commitish: ${{ github.sha }}
================================================
FILE: .github/workflows/alpha_build_manually_without_sign.yml
================================================
name: Alpha Build Manually (Without signature)
on:
workflow_dispatch:
inputs:
google_services_json:
description: "google-services.json (optional)"
jobs:
build-alpha:
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: develop
fetch-depth: 0
submodules: 'true'
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
java-version: '21'
distribution: 'temurin'
- name: Write google-services.json
env:
DATA: ${{ github.event.inputs.google_services_json }}
run: echo $DATA > app/google-services.json
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build apk
run: ./gradlew assembleDefaultAlpha assembleDefaultDebug
- name: Read alpha apk output metadata
id: apk-meta-alpha
uses: juliangruber/read-file-action@v1
with:
path: app/build/outputs/apk/default/alpha/output-metadata.json
- name: Read alpha debug apk output metadata
id: apk-meta-alpha-debug
uses: juliangruber/read-file-action@v1
with:
path: app/build/outputs/apk/default/debug/output-metadata.json
- name: Parse apk infos
id: apk-infos
run: |
echo "alpha_info_version_code=${{ fromJson(steps.apk-meta-alpha.outputs.content).elements[0].versionCode }}" >> $GITHUB_ENV
echo "alpha_info_version_name=${{ fromJson(steps.apk-meta-alpha.outputs.content).elements[0].versionName }}" >> $GITHUB_ENV
echo "alpha_debug_info_version_code=${{ fromJson(steps.apk-meta-alpha-debug.outputs.content).elements[0].versionCode }}" >> $GITHUB_ENV
echo "alpha_debug_info_version_name=${{ fromJson(steps.apk-meta-alpha-debug.outputs.content).elements[0].versionName }}" >> $GITHUB_ENV
# upload artifacts default-debug
- name: Archive default debug build artifacts (universal)
uses: actions/upload-artifact@v4
with:
name: Default debug build artifact (universal)
path: app/build/outputs/apk/default/debug/BV_${{ env.alpha_debug_info_version_code }}_${{ env.alpha_debug_info_version_name }}_default_universal.apk
# upload artifacts default-alpha
- name: Archive default alpha build mappings
uses: actions/upload-artifact@v4
with:
name: Default alpha build mappings
path: app/build/outputs/mapping/defaultAlpha
- name: Archive default alpha build artifacts (universal)
uses: actions/upload-artifact@v4
with:
name: Default alpha build artifact (universal)
path: app/build/outputs/apk/default/alpha/BV_${{ env.alpha_info_version_code }}_${{ env.alpha_info_version_name }}_default_universal.apk
================================================
FILE: .github/workflows/auto_close_issues.yml
================================================
name: Check Issues
on:
issues:
types: [ opened ]
jobs:
check:
runs-on: ubuntu-latest
steps:
- if: contains(github.event.issue.title, '以简单的一段字概括' )
id: close-invalid-title
name: Close Issue (invalid title)
uses: actions-cool/issues-helper@v3
with:
actions: 'add-labels,close-issue'
token: ${{ secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.issue.number }}
labels: '无效'
close-reason: 'not_planned'
- if: contains(github.event.issue.body, '我正在使用旧版本' )
id: close-old-version
name: Close Issue (old version)
uses: actions-cool/issues-helper@v3
with:
actions: 'create-comment,close-issue'
token: ${{ secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.issue.number }}
close-reason: 'not_planned'
body: 请先尝试使用当前最新 alpha(或release) 版本,如果问题依然存在再提交 issue
================================================
FILE: .github/workflows/close_inactive_issues.yml
================================================
name: Close inactive issues
on:
schedule:
- cron: "30 1 * * *"
jobs:
close-issues:
name: Close inactive issues
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: actions/stale@v5
with:
days-before-issue-stale: 60
days-before-issue-close: 14
days-before-pr-stale: -1
stale-issue-label: "过时"
stale-issue-message: "该 issue 已过时,因为它已经超过 60 天没有任何活动"
close-issue-message: "该 issue 已关闭,因为它在被标记为过时后 14 天依旧没有任何活动"
repo-token: ${{ secrets.GITHUB_TOKEN }}
exempt-issue-labels: "bug,新功能,优化,有待讨论,疑难杂症"
================================================
FILE: .github/workflows/features.yml
================================================
name: Feature Build
on:
push:
branches:
- 'feature/**'
jobs:
build-alpha:
name: Build Feature Apk
runs-on: macos-latest
if: github.repository == 'aaa1115910/bv'
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
fetch-depth: 0
submodules: 'true'
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
java-version: '21'
distribution: 'temurin'
- name: Write google-services.json
env:
DATA: ${{ secrets.GOOGLE_SERVICES_JSON }}
run: echo $DATA > app/google-services.json
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Add signing properties
env:
SIGNING_PROPERTIES: ${{ secrets.SIGNING_PROPERTIES }}
run: |
echo ${{ secrets.SIGNING_PROPERTIES }} > encoded_signing_properties
base64 -Dd -i encoded_signing_properties > signing.properties
- name: Add jks file
run: |
echo ${{ secrets.SIGN_KEY }} > ./encoded_key
base64 -Dd -i encoded_key > key.jks
- name: Build apk
run: ./gradlew assembleDefaultAlpha assembleDefaultDebug
- name: Read alpha apk output metadata
id: apk-meta-alpha
uses: juliangruber/read-file-action@v1
with:
path: app/build/outputs/apk/default/alpha/output-metadata.json
- name: Read alpha debug apk output metadata
id: apk-meta-alpha-debug
uses: juliangruber/read-file-action@v1
with:
path: app/build/outputs/apk/default/debug/output-metadata.json
- name: Parse apk infos
id: apk-infos
run: |
echo "alpha_info_version_code=${{ fromJson(steps.apk-meta-alpha.outputs.content).elements[0].versionCode }}" >> $GITHUB_ENV
echo "alpha_info_version_name=${{ fromJson(steps.apk-meta-alpha.outputs.content).elements[0].versionName }}" >> $GITHUB_ENV
echo "alpha_debug_info_version_code=${{ fromJson(steps.apk-meta-alpha-debug.outputs.content).elements[0].versionCode }}" >> $GITHUB_ENV
echo "alpha_debug_info_version_name=${{ fromJson(steps.apk-meta-alpha-debug.outputs.content).elements[0].versionName }}" >> $GITHUB_ENV
# upload artifacts alpha debug
- name: Archive alpha debug build artifacts
uses: actions/upload-artifact@v4
with:
name: Alpha debug build artifact
path: app/build/outputs/apk/default/debug/BV_${{ env.alpha_debug_info_version_code }}_${{ env.alpha_debug_info_version_name }}_default_universal.apk
# upload artifacts alpha
- name: Archive alpha build mappings
uses: actions/upload-artifact@v4
with:
name: Alpha build mappings
path: app/build/outputs/mapping/defaultAlpha
- name: Archive alpha build artifacts
uses: actions/upload-artifact@v4
with:
name: Alpha build artifact
path: app/build/outputs/apk/default/alpha/BV_${{ env.alpha_info_version_code }}_${{ env.alpha_info_version_name }}_default_universal.apk
================================================
FILE: .github/workflows/release.yml
================================================
name: Release Build
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
- 'v[0-9]+.[0-9]+.[0-9]+.[0-9]+'
jobs:
build-release:
name: Build Release Apk
runs-on: macos-latest
if: github.repository == 'aaa1115910/bv'
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: master
fetch-depth: 0
submodules: 'true'
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
java-version: '21'
distribution: 'temurin'
- name: Write google-services.json
env:
DATA: ${{ secrets.GOOGLE_SERVICES_JSON }}
run: echo $DATA > app/google-services.json
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Add signing properties
env:
SIGNING_PROPERTIES: ${{ secrets.SIGNING_PROPERTIES }}
run: |
echo ${{ secrets.SIGNING_PROPERTIES }} > encoded_signing_properties
base64 -Dd -i encoded_signing_properties > signing.properties
- name: Add jks file
run: |
echo ${{ secrets.SIGN_KEY }} > ./encoded_key
base64 -Dd -i encoded_key > key.jks
- name: Build apk
run: ./gradlew assembleDefaultRelease assembleDefaultDebug
- name: Read release apk output metadata
id: apk-meta-release
uses: juliangruber/read-file-action@v1
with:
path: app/build/outputs/apk/default/release/output-metadata.json
- name: Read debug apk output metadata
id: apk-meta-release-debug
uses: juliangruber/read-file-action@v1
with:
path: app/build/outputs/apk/default/debug/output-metadata.json
- name: Parse apk infos
id: apk-infos
run: |
echo "release_info_version_code=${{ fromJson(steps.apk-meta-release.outputs.content).elements[0].versionCode }}" >> $GITHUB_ENV
echo "release_info_version_name=${{ fromJson(steps.apk-meta-release.outputs.content).elements[0].versionName }}" >> $GITHUB_ENV
echo "release_debug_info_version_code=${{ fromJson(steps.apk-meta-release-debug.outputs.content).elements[0].versionCode }}" >> $GITHUB_ENV
echo "release_debug_info_version_name=${{ fromJson(steps.apk-meta-release-debug.outputs.content).elements[0].versionName }}" >> $GITHUB_ENV
- name: Get changelog
id: changelog
run: |
{
echo "changelog<<EOF"
echo "$(git log --pretty=format:"- %s (%h)" ${{ github.event.before }}..${{ github.sha }})"
echo "EOF"
} >> "$GITHUB_ENV"
# upload artifacts release debug
- name: Archive release debug build artifacts
uses: actions/upload-artifact@v4
with:
name: Release debug build artifact
path: app/build/outputs/apk/default/debug/BV_${{ env.release_debug_info_version_code }}_${{ env.release_debug_info_version_name }}_default_universal.apk
# upload artifacts release
- name: Archive release build mappings
uses: actions/upload-artifact@v4
with:
name: Release build mappings
path: app/build/outputs/mapping/defaultRelease
- name: Archive release build artifacts
uses: actions/upload-artifact@v4
with:
name: Release build artifact
path: app/build/outputs/apk/default/release/BV_${{ env.release_info_version_code }}_${{ env.release_info_version_name }}_default_universal.apk
# zip mapping because softprops/action-gh-release can't upload folder
- name: Zip mapping
run: zip -rj mapping.zip app/build/outputs/mapping/defaultRelease
# upload to github release
- name: Publish Release
uses: softprops/action-gh-release@v2
with:
files: |
app/build/outputs/apk/default/debug/BV_${{ env.release_debug_info_version_code }}_${{ env.release_debug_info_version_name }}_default_universal.apk
app/build/outputs/apk/default/release/BV_${{ env.release_info_version_code }}_${{ env.release_info_version_name }}_default_universal.apk
mapping.zip
tag_name: ${{ github.ref_name }}
name: ${{ env.release_info_version_name }}
prerelease: false
body: ${{ env.changelog }}
target_commitish: ${{ github.sha }}
================================================
FILE: .gitignore
================================================
*.iml
.idea
.gradle
/local.properties
.DS_Store
build
/captures
.externalNativeBuild
.cxx
/signing.properties
/.idea/jarRepositories.xml
/.idea/migrations.xml
/.idea/codeStyles/
.kotlin
.vscode
buildSrc
bin
keystore.jks
signing.properties
*.log
/app/default/
================================================
FILE: .gitmodules
================================================
[submodule "libs"]
path = libs
url = https://github.com/aaa1115910/bv-libs.git
================================================
FILE: CHANGELOG.md
================================================
[](https://github.com/fantasytyx/bv/releases)
- 主屏(整体框架以及首页、UGC、PGC)
- 重构左侧导航栏,移除抽屉展开效果
- 重写首页列表、UGC列表,优化性能
- 首页、ugc:标题改为2行、缩减视频列表的间距
- 首页、ugc:视频卡片显示发布时间
- 首页、ugc:视频卡片修改选中效果
- 记住每个内容页当前选中的Tab并在切换回来后恢复
- 支持按返回键回到左侧菜单栏
- 首页、ugc、pgc:顶部Tab切换增加防抖,延迟发起内容和请求
- 首页、ugc、pgc:简化内容区切换动画;让容器填充满屏幕,避免竖向的收缩动画
- 首页、ugc、pgc:按返回键定位到顶部tab时,内容区不滚动到顶部(点击tab刷新数据会回到顶部)
- UGC:缓存每个tab的数据,减少请求次数
- UGC:去掉功能并没实现的子分类
- 左侧菜单项未获得焦点的时候,去掉选中效果
- 左侧菜单切换增加防抖,延迟切换右侧内容
- 把“浏览历史、我的收藏、我的追番、稍后再看”整合到“首页”下面
- 点击左侧菜单的“用户头像”改成进入“用户中心”页面
- 优化各个列表焦点元素停留位置,让能显示更多完整项
- 解决番剧时间线页面无法显示的问题
- 首页的推荐、热门、动态、历史、收藏、稍后再看列表、UGC列表以及UGC视频推荐列表,UGC视频卡片增加长按确认键进入up主空间页面
- 动态,聚焦在视频卡片上时,按菜单键打开UP关注列表页
- 动态、up空间、视频推荐,在充电视频的UGC视频卡片右上角增加闪电图标(web接口)
- 去掉列表顶部tab的padding动画
- 播放详情页
- 支持不显示UGC视频详情页,直接播放
- 增加点赞、投币
- 调整收藏、简介入口位置
- 修改视频封面,合集中的视频改成显示视频封面。原逻辑合集中的视频显示合集的封面
- 选中封面增加边框
- 多个收藏夹时,弹窗让用户选择要添加到哪个收藏夹,仅单个收藏夹时不弹窗直接加入默认收藏夹
- 视频详情页增加实例管理逻辑,最多保留3个实例,优化内存占用,同时也更少返回次数就能回到列表页
- 回退页面的时不请求视频关联的用户数据(点赞、投币、收藏),减少请求时有必要的
- 修改合集弹窗列表的序号错误
- 左上角视频封面图的右下角增加视频时长
- 视频卡片中的视频时长新增小时部分,小时部分在时长超过60分钟时出现
- 背景图从优先取合集封面改成取视频封面
- 背景图显隐增加淡入淡出动画
- 播放页
- 增加“推荐视频”
- 操作方式: 1)双击下键; 2)按下键显示视频信息,移动焦点在底部那排按钮后再按下键
- 播放器控制条增加点赞、收藏、投币(仅UGC视频且要登录才会显示)
- 播放器控制条,默认聚焦在进度条
- 此时,按确认键会触发“播放/暂停”、按左右键回触发“快进/快退”
- 播放器控制条,增加功能按钮(播放速度、up空间、旋转画面、字幕开关、刷新当前视频、弹幕开关、播放清单、推荐视频、播放器设置、循环播放)
- UGC视频才会显示 up空间入口。会根据是否已关注显示不同的图标
- 有字幕才显示 字幕开关
- 新增播放器底部常驻进度条功能和配置
- 视频信息,调小标题字体、调小进度条高度、调浅缓冲进度颜色
- 播放速度调成"画面音频"子菜单的第一个
- 播放速度生效周期改成单个视频,即切视频就重置为1(原版播放速度是全局存储的)
- 播放速度增加倍数 2.25、2.5、2.75、3
- 音频编码调到画面比例前面
- 弹幕设置默认值改成 字体缩放110%、不透明度80%、显示区域20%
- 增加按下返回键隐藏控制条
- 增加快进/快退一段时间无操作自动确认播放
- 控制条中的视频时长新增小时部分,小时部分在时长超过60分钟时出现
- 支持“竖屏视频播放时的最大清晰度为1080P”(解决部分设备竖屏视频变形的问题)
- 支持“都播完后退出播放器”,退出后是回到视频详情页
- 支持隐藏视频播放页面左下角的视频调试信息
- 优化视频缓冲逻辑,缓解卡面卡死
- 支持当前视频播放完后不播放下一个分P视频或合集视频
- 给“自动播放下一个视频和自动退出”增加提示。倒数结束前可按任意键取消执行
- 调整标题显示样式,正确显示视频的标题(投稿名称)和副标题(分p名称)。(如果视频只有单P时不显示副标题)
- 修复字幕错乱bug
- UGC视频的视频信息增加up主名称、播放数、弹幕数、收藏数、投币数、点赞数、发布时间
- 新增支持切换分P/分集视频后返回到当前视频的详情页
- 修复非AI自动生成的字幕加载失败的问题
- 播放器控制条,默认聚焦在进度条,按左右键“快进/快退”、按确认键“暂停/播放”
- 无法播放的视频不上报历史
- 新增视频画面旋转功能
- 搜索
- 搜索结果页视频卡片显示发布时间
- 支持按返回键回到左侧菜单栏
- 搜索结果页改成选中tab的时候才发起请求,以便减少请求次数
- up空间页
- 视频卡片显示发布时间
- 增加 关注up 功能
- 修改页面历史记录,允许记录最多两个不同up的空间页
- 收藏
- 视频卡片显示 收藏时间
- 浏览历史
- 视频卡片显示 浏览时间
- 标签搜索结果页
- 视频卡片显示发布时间
- 设置页面
- 增加分类“播放设置”
- 界面设置-增加“首页默认标签”设置,默认“推荐”
- 可以修改打开应用时首页默认选中的标签
- 播放设置-增加是否“显示UGC视频详情页”设置,默认显示
- 关闭后,点击非PGC视频卡片不显示详情页直接开始播放
- 播放设置-增加设置“显示视频加载信息”,默认不显示
- 播放设置-增加设置“设置竖屏视频播放时的最大清晰度为1080P”,默认禁用
- 开启可解决部分设备竖屏视频变形/花屏的问题
- 播放设置-增加是否“自动播放下一个视频”设置,默认开启
- 播放设置-增加是否“都播完后退出播放器”设置,默认开启
- 播放设置-增加默认播放速度配置,默认1倍
- 播放设置-增加快进时间间隔配置,默认10秒
- 播放设置-增加快退时间间隔配置,默认5秒
- 播放设置-增加是否显示“播放器底部常驻进度条”配置,默认不显示
- 关于-更新,优化应用更新弹窗在内容很多时支持滚动查看
- 关于-更新,使用github镜像源加速下载
- 其它
- 播放量改成Long类型,解决追番列表无法显示(凡人播放量超过Int类型的最大值)
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2022 aaa1115910
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
<div align="center">
<img src="app/shared/src/main/res/drawable/ic_banner_md.webp" style="border-radius: 24px; margin-top: 32px;"/>
# BV
~~Bug Video~~
[](https://apilevels.com/#:~:text=Jetpack%20Compose%20requires%20a%20minSdk%20of%2021%20or%20higher)
[](https://github.com/fantasytyx/bv)
**BV 无法在中国大陆地区内的智能电视上使用,如有相关使用需求请使用 [云视听小电视](https://app.bilibili.com)**
**禁止在中国境内传播、宣传、分发 BV**
</div>
---
BV ~~(Bug Video)~~ 是一款 [哔哩哔哩](https://www.bilibili.com) 的第三方应用,适配 `Android 移动端`
和 `Android TV`,使用 `Jetpack Compose` 开发
**都是随心乱写的代码,能跑就行。**
---
<div align="center">
# 学废了
</div>
## 声明
**此项目是个人为了学习安卓开发而fork, 仅用于学习和测试,禁止在中国境内传播、宣传、分发,如有相关使用需求请使用 [哔哩哔哩官方APP](https://app.bilibili.com),否则后果自负**
## 修改
在原bv的基础上做了一些修改,包括:
- 把“浏览历史、我的收藏、我的追番、稍后再看”整合到“首页”下面
- 增加“首页默认标签”设置 (设置-界面设置,默认“推荐”)
- **可以修改打开应用时首页默认选中的标签**,选项有:推荐、热门、动态、历史、收藏、追番、稍后再看
- 首页推荐、热门、动态、历史、收藏、稍后再看,UGC列表以及UGC视频推荐列表,可以**在UGC视频卡片长按确认键进入up主空间页面**看up的所有投稿视频
- **动态页面**,聚焦在视频卡片上时,**按菜单键打开已关注UP列表页**,可以筛选想看的up
- 动态、up空间、视频推荐,在充电视频的UGC视频卡片右上角增加闪电图标(web接口)
- 在主屏右上角显示当前时间
- 首页导航项、UGC导航项、PGC导航项支持自定义排序和隐藏
- 设置-界面设置
- **添加直播**,推荐、关注、分区,直播搜索,直播弹幕
- UGC详情页、PGC详情页、UGC&PGC视频播放页 增加评论功能
- 支持两种导航切换模式:聚焦后自动切换、聚焦并确认才切换
- 支持长按确认键加速播放
- 浏览历史、收藏、追番、稍后再看 这4个列表 新增删除支持。按菜单键进入删除模式,长按(或短按)确认键删除当前选中项,按返回键退出删除模式

- **UGC视频详情页增加点赞、投币功能**
- 增加是否“显示UGC视频详情页”设置 (默认显示)
- 关闭后,点击UGC视频卡片会**跳过详情页直接开始播放**
- 合集/分P 自动滚动到最后播放的视频并高亮显示

- **播放器页面增加“推荐视频”、“视频列表”**
- 操作方式: 1)双击下方向键; 2)按下键显示视频信息,移动焦点在底部那排按钮后再按下方向键

- **新增视频画面旋转功能**
- 播放器控制条,**增加点赞、收藏、投币**
- 仅UGC视频且要登录才会显示
- 播放器控制条,默认聚焦在进度条
- 此时,按确认键会触发“播放/暂停”、按左右键回触发“快进/快退”
- 播放器控制条,增加功能按钮(播放速度、画质、up空间、画面旋转、字幕开关、重新加载当前视频、弹幕开关、循环播放、播放清单、推荐视频、视频简介、播放器设置)
- 新增识别字幕类型,添加AI标识
- 播放器控制条,支持设置按钮的顺序、显隐、默认焦点
- 支持PGC视频自动跳过片头/片尾设置
- 支持播放只有音轨的视频
- 换成新版弹幕接口

- 调整设置,增加分类“播放设置”
- 把 分辨率、视频编码、音频编码、启用音频软件 4个设置移入这个分类
- 增加是否“显示UGC视频详情页”设置 (默认显示)
- 增加是否在播放页面底部 常驻“显示**迷你进度条**”设置(默认不显示)
- 增加“显示视频加载过程信息”设置(默认不显示)
- **增加“竖屏视频播放异常时的处理**方式”设置(默认不处理)
- 不是所有设备都有问题,没问题的同学不要开;
- 使用TextureView模式卡的不行的,建议用限制到1080P的模式
- **增加“下一个播放”设置**(默认不播放),可设置为:
- 不播
- 播推荐视频
- 播剧集和分P的下一个
- 播播剧集和分P的下一个或推荐视频
- 增加是否“都播完后退出播放器”设置(默认开启)
- 增加默认播放速度设置(默认1倍)
- 增加快进时间间隔设置(默认10秒)
- 增加快退时间间隔设置(默认5秒)
- 增加显示在线观看人数(默认一直显示,可选不显示、30秒后隐藏)
- 增加开始播放位置设置(默认从头开始播放,可选从历史位置播放)
- 新增PGC视频自动跳过片头/片尾设置(默认关闭)
- 新增 播放器控制按钮支持排序、显隐、设置默认焦点
- 新增 点播与直播的弹幕过滤等级设置
- 新增 播放器页面长按确认键的执行动作的设置,可选:打开菜单、加速播放
- 新增 长按确认键加速播放加速值的设置,默认2x
- 界面设置
- 新增 界面模式设置,选项有:启动时自动检测、强制使用 TV,或强制使用 Mobile 界面(默认自动检测)
- 新增页面浏览历史相关的设置:UGC 视频详情页面的历史记录数量、详情页历史记录是否包含播放器打开的详情页、UGC 视频播放页面的历史记录数量
- 新增 UGC导航项设置,可修改顺序和显隐
- 新增 PGC导航项设置,可修改顺序和显隐
- 新增 直播导航项设置,可修改顺序和显隐
- 新增 导航切换模式设置,选项有:聚焦后自动切换、聚焦并确认才切换
- 网络设置
- 新增 仅允许IPV4的选项

- 优化up空间页,丰富内容并增加关注功能
- 优化已关注up列表页,增加本地搜索
- 优化搜索页面、账号管理页面
- 优化列表、优化视频卡片显示更多内容、精简动画、增加数据缓存、减少非必要的请求
- 按自己的喜好调整页面的布局、元素大小、交互方式、原有功能
- 解决一些bug等等
## 构建
自己动手丰衣足食
- 安装开发环境
- Android studio、Android SDK、JAVA等等
- 补全构建需要的文件
- 在项目根目录用使用 Android SDK 中的 keytool 工具创建签名文件 keystore.jks。
```sh
keytool -genkey -v -keystore keystore.jks -alias 别名 -keyalg RSA -keysize 2048 -validity 10000
```
命令说明:
- genkey: 生成密钥对
- -v: 详细输出
- -keystore keystore.jks: 指定生成的密钥库文件名
- -alias 别名: 指定密钥的别名(可以根据需要修改)
- -keyalg RSA: 使用 RSA 算法
- -keysize 2048: 密钥长度为 2048 位
- -validity 10000: 密钥的有效期为 10000 天(约 27 年)
执行此命令后,会提示你输入:
- 密钥库密码(keystore.pwd)
- 密钥密码(keystore.alias_pwd),可以与密钥库密码相同
- 姓名、组织单位、城市等信息,可空
- 在项目根目录增加 signing.properties 文件。文件内容如下
```properties
keystore.path=./keystore.jks
keystore.pwd=创建签名文件时设置的密码
keystore.alias=创建签名文件时设置的别名
keystore.alias_pwd=创建签名文件时设置的别名密码
```
2. 执行构建命令来生成 apk 文件
```sh
# release
./gradlew clean assembleRelease
```
- 在根目录增加 signing.properties 文件。文件内容如下
```properties
keystore.path=./keystore.jks
keystore.pwd=创建签名文件时设置的密码
keystore.alias=创建签名文件时设置的别名
keystore.alias_pwd=创建签名文件时设置的别名密码
```
- 执行构建命令来生成 apk 文件
```sh
# release
./gradlew clean assembleRelease
```
## 安装
### adb
./adb.exe connect 192.168.xx.xx
./adb.exe -s 192.168.xx.xx install -r -d {apk文件路径}
### Release
- [Github Release](https://github.com/fantasytyx/bv/releases)
## License
[MIT](LICENSE) © aaa1115910
================================================
FILE: app/.gitignore
================================================
/build
/google-services.json
/release
/r8Test
/debug
================================================
FILE: app/build.gradle.kts
================================================
@file:Suppress("UnstableApiUsage")
import com.android.build.gradle.internal.api.ApkVariantOutputImpl
import java.io.FileInputStream
import java.util.Properties
plugins {
alias(gradleLibs.plugins.android.application)
alias(gradleLibs.plugins.compose.compiler)
alias(gradleLibs.plugins.google.ksp)
alias(gradleLibs.plugins.kotlin.android)
alias(gradleLibs.plugins.kotlin.serialization)
}
val signingProp = file(project.rootProject.file("signing.properties"))
android {
signingConfigs {
if (signingProp.exists()) {
val properties = Properties().apply {
load(FileInputStream(signingProp))
}
create("key") {
storeFile = rootProject.file(properties.getProperty("keystore.path"))
storePassword = properties.getProperty("keystore.pwd")
keyAlias = properties.getProperty("keystore.alias")
keyPassword = properties.getProperty("keystore.alias_pwd")
}
}
}
namespace = AppConfiguration.appId
compileSdk = AppConfiguration.compileSdk
defaultConfig {
applicationId = AppConfiguration.applicationId
minSdk = AppConfiguration.minSdk
targetSdk = AppConfiguration.targetSdk
versionCode = AppConfiguration.versionCode
versionName = AppConfiguration.versionName
vectorDrawables {
useSupportLibrary = true
}
}
flavorDimensions.add("channel")
productFlavors {
// create("lite") {
// dimension = "channel"
// }
create("default") {
dimension = "channel"
}
}
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true // 移除未使用的资源
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
if (signingProp.exists()) signingConfig = signingConfigs.getByName("key")
}
debug {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
applicationIdSuffix = ".debug"
}
create("r8Test") {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
applicationIdSuffix = ".r8test"
if (signingProp.exists()) signingConfig = signingConfigs.getByName("key")
}
create("alpha") {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
if (signingProp.exists()) signingConfig = signingConfigs.getByName("key")
}
}
buildFeatures {
compose = true
//buildConfig = true
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
excludes += "**/*.proto"
excludes += "**/*.kotlin_metadata"
excludes += "**/kotlin/**"
excludes += "**/*.txt"
excludes += "**/*.version"
}
// if (gradle.startParameter.taskNames.find { it.startsWith("assembleLite") } != null) {
// jniLibs {
// val vlcLibs = listOf("libvlc", "libc++_shared", "libvlcjni")
// val abis = listOf("x86_64", "x86", "arm64-v8a", "armeabi-v7a")
// vlcLibs.forEach { vlcLibName -> abis.forEach { abi -> excludes.add("lib/$abi/$vlcLibName.so") } }
// }
// }
}
/*splits {
if (gradle.startParameter.taskNames.find { it.startsWith("assembleDefault") } != null) {
abi {
isEnable = true
reset()
include("x86_64", "x86", "arm64-v8a", "armeabi-v7a")
isUniversalApk = true
}
}
}*/
applicationVariants.configureEach {
val variant = this
outputs.configureEach {
(this as ApkVariantOutputImpl).apply {
val abi = this.filters.find { it.filterType == "ABI" }?.identifier ?: "universal"
outputFileName =
"BV_${AppConfiguration.versionCode}_${AppConfiguration.versionName}.${variant.buildType.name}_${variant.flavorName}_$abi.apk"
versionNameOverride =
"${variant.versionName}.${variant.buildType.name}"
}
}
}
}
composeCompiler {
reportsDestination = layout.buildDirectory.dir("compose_build_reports")
stabilityConfigurationFiles.addAll(
layout.projectDirectory.file("compose_compiler_config.conf")
)
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(AppConfiguration.jdk))
}
}
dependencies {
implementation(project(":app:mobile"))
implementation(project(":app:tv"))
implementation(project(":app:shared"))
}
tasks.withType<Test> {
useJUnitPlatform()
}
================================================
FILE: app/compose_compiler_config.conf
================================================
kotlin.collections.*
kotlin.time.Duration
kotlinx.coroutines.CoroutineScope
androidx.paging.compose.LazyPagingItems
# 核心稳定性
androidx.compose.runtime.Composable
androidx.compose.runtime.State
androidx.compose.ui.Modifier
# TV 特定
androidx.tv.material3.Button
androidx.tv.material3.Card
androidx.tv.material3.Surface
# 项目特定
dev.aaa1115910.bv.tv.screens.main.*
================================================
FILE: app/mobile/.gitignore
================================================
/build
================================================
FILE: app/mobile/build.gradle.kts
================================================
plugins {
alias(gradleLibs.plugins.android.library)
alias(gradleLibs.plugins.compose.compiler)
alias(gradleLibs.plugins.google.ksp)
alias(gradleLibs.plugins.kotlin.android)
alias(gradleLibs.plugins.kotlin.serialization)
}
android {
namespace = AppConfiguration.appId + ".mobile"
compileSdk = AppConfiguration.compileSdk
defaultConfig {
minSdk = AppConfiguration.minSdk
vectorDrawables {
useSupportLibrary = true
}
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
create("r8Test") {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
create("alpha") {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
buildFeatures {
compose = true
}
lint {
targetSdk = AppConfiguration.targetSdk
}
testOptions {
targetSdk = AppConfiguration.targetSdk
}
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(AppConfiguration.jdk))
}
}
dependencies {
implementation(project(":app:shared"))
}
================================================
FILE: app/mobile/consumer-rules.pro
================================================
================================================
FILE: app/mobile/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: app/mobile/src/main/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name="dev.aaa1115910.bv.mobile.activities.MainActivity"
android:exported="true"
android:theme="@style/Theme.BV.Mobile.Splash">
</activity>
<activity
android:name=".activities.VideoPlayerActivity"
android:configChanges="orientation|screenSize"
android:exported="true"
android:label="@string/title_mobile_activity_video_player"
android:theme="@style/Theme.BV.Mobile" />
<activity
android:name=".activities.LoginActivity"
android:exported="false"
android:label="@string/title_mobile_activity_login"
android:theme="@style/Theme.BV.Mobile" />
<activity
android:name=".activities.UserSpaceActivity"
android:exported="false"
android:label="@string/title_mobile_activity_user_space"
android:theme="@style/Theme.BV.Mobile" />
<activity
android:name=".activities.SettingsActivity"
android:exported="false"
android:label="@string/title_mobile_activity_settings"
android:theme="@style/Theme.BV.Mobile" />
<activity
android:name=".activities.DynamicDetailActivity"
android:exported="false"
android:label="@string/title_mobile_activity_dynamic_detail"
android:theme="@style/Theme.BV.Mobile" />
<activity
android:name=".activities.FollowingUserActivity"
android:exported="false"
android:label="@string/title_mobile_activity_following_user"
android:theme="@style/Theme.BV.Mobile" />
<activity
android:name=".activities.HistoryActivity"
android:exported="false"
android:label="@string/title_mobile_activity_history"
android:theme="@style/Theme.BV.Mobile" />
<activity
android:name=".activities.FavoriteActivity"
android:exported="false"
android:label="@string/title_mobile_activity_favorite"
android:theme="@style/Theme.BV.Mobile" />
<activity
android:name=".activities.FollowingSeasonActivity"
android:exported="false"
android:label="@string/title_mobile_activity_following_season"
android:theme="@style/Theme.BV.Mobile" />
<activity
android:name=".activities.IntentHandlerActivity"
android:exported="true"
android:label="@string/title_mobile_activity_intent_handler"
android:launchMode="singleTask"
android:theme="@style/Theme.BV.Mobile">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="qrtoken"
android:scheme="bugvideo" />
</intent-filter>
</activity>
<activity
android:name=".activities.QrTokenResultActivity"
android:exported="false"
android:label="@string/title_mobile_activity_qr_token_result"
android:theme="@style/Theme.BV.Mobile" />
</application>
</manifest>
================================================
FILE: app/mobile/src/main/kotlin/com/origeek/imageViewer/gallery/ImageGallery.kt
================================================
package com.origeek.imageViewer.gallery
import androidx.annotation.FloatRange
import androidx.annotation.IntRange
import androidx.compose.foundation.interaction.InteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import com.origeek.imageViewer.previewer.DEFAULT_ITEM_SPACE
import com.origeek.imageViewer.viewer.ImageViewer
import com.origeek.imageViewer.viewer.ImageViewerState
import com.origeek.imageViewer.viewer.rememberViewerState
import kotlinx.coroutines.launch
/**
* @program: ImageViewer
*
* @description:
*
* @author: JVZIYAOYAO
*
* @create: 2022-10-10 11:50
**/
/**
* gallery手势对象
*/
class GalleryGestureScope(
// 点击事件
var onTap: () -> Unit = {},
// 双击事件
var onDoubleTap: () -> Boolean = { false },
// 长按事件
var onLongPress: () -> Unit = {},
)
/**
* gallery图层对象
*/
class GalleryLayerScope(
// viewer图层
var viewerContainer: @Composable (
page: Int, viewerState: ImageViewerState, viewer: @Composable () -> Unit
) -> Unit = { _, _, viewer -> viewer() },
// 背景图层
var background: @Composable ((Int) -> Unit) = {},
// 前景图层
var foreground: @Composable ((Int) -> Unit) = {},
)
/**
* gallery状态
*/
open class ImageGalleryState(
val pagerState: ImagePagerState,
) {
/**
* 当前viewer的状态
*/
var imageViewerState by mutableStateOf<ImageViewerState?>(null)
internal set
/**
* 当前页码
*/
val currentPage: Int
get() = pagerState.currentPage
/**
* 目标页码
*/
val targetPage: Int
get() = pagerState.targetPage
/**
* interactionSource
*/
val interactionSource: InteractionSource
get() = pagerState.interactionSource
/**
* 滚动到指定页面
* @param page Int
* @param pageOffset Float
*/
suspend fun scrollToPage(
@IntRange(from = 0) page: Int,
@FloatRange(from = 0.0, to = 1.0) pageOffset: Float = 0f,
) = pagerState.scrollToPage(page, pageOffset)
/**
* 动画滚动到指定页面
* @param page Int
* @param pageOffset Float
*/
suspend fun animateScrollToPage(
@IntRange(from = 0) page: Int,
@FloatRange(from = 0.0, to = 1.0) pageOffset: Float = 0f,
) = pagerState.animateScrollToPage(page, pageOffset)
}
/**
* 记录gallery状态
*/
@Composable
fun rememberImageGalleryState(
@IntRange(from = 0) initialPage: Int = 0,
pageCount: () -> Int,
): ImageGalleryState {
val imagePagerState = rememberImagePagerState(initialPage, pageCount)
return remember { ImageGalleryState(imagePagerState) }
}
/**
* 图片gallery,基于Pager实现的一个图片查看列表组件
*/
@Composable
fun ImageGallery(
// 编辑参数
modifier: Modifier = Modifier,
// gallery状态
state: ImageGalleryState,
// 图片加载器
imageLoader: @Composable (Int) -> Any?,
// 每张图片之间的间隔
itemSpacing: Dp = DEFAULT_ITEM_SPACE,
// 检测手势
detectGesture: GalleryGestureScope.() -> Unit = {},
// gallery图层
galleryLayer: GalleryLayerScope.() -> Unit = {},
) {
// require(count >= 0) { "imageCount must be >= 0" }
val scope = rememberCoroutineScope()
// 手势相关
val galleryGestureScope = remember { GalleryGestureScope() }
detectGesture.invoke(galleryGestureScope)
// 图层相关
val galleryLayerScope = remember { GalleryLayerScope() }
galleryLayer.invoke(galleryLayerScope)
// 确保不会越界
val currentPage = state.currentPage
Box(
modifier = modifier
.fillMaxSize()
) {
galleryLayerScope.background(currentPage)
ImageHorizonPager(
state = state.pagerState,
modifier = Modifier
.fillMaxSize(),
itemSpacing = itemSpacing,
) { page ->
val imageState = rememberViewerState()
LaunchedEffect(key1 = currentPage) {
if (currentPage != page) imageState.reset()
if (currentPage == page) {
state.imageViewerState = imageState
}
}
galleryLayerScope.viewerContainer(page, imageState) {
Box(
modifier = Modifier
.fillMaxSize(),
) {
key(page) {
ImageViewer(
modifier = Modifier.fillMaxSize(),
model = imageLoader(page),
state = imageState,
boundClip = false,
detectGesture = {
this.onTap = {
galleryGestureScope.onTap()
}
this.onDoubleTap = {
val consumed = galleryGestureScope.onDoubleTap()
if (!consumed) scope.launch {
imageState.toggleScale(it)
}
}
this.onLongPress = { galleryGestureScope.onLongPress() }
},
)
}
}
}
}
galleryLayerScope.foreground(currentPage)
}
}
================================================
FILE: app/mobile/src/main/kotlin/com/origeek/imageViewer/gallery/ImagePager.kt
================================================
package com.origeek.imageViewer.gallery
import androidx.annotation.FloatRange
import androidx.annotation.IntRange
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.interaction.InteractionSource
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
/**
* @program: ImageViewer
*
* @description:
*
* @author: JVZIYAOYAO
*
* @create: 2022-10-05 21:41
**/
/**
* 基于HorizonPager封装的pager组件
*/
open class ImagePagerState @OptIn(ExperimentalFoundationApi::class) constructor(
val pagerState: PagerState,
) {
/**
* 当前页码
*/
@OptIn(ExperimentalFoundationApi::class)
val currentPage: Int
get() = pagerState.currentPage
/**
* 目标页码
*/
@OptIn(ExperimentalFoundationApi::class)
val targetPage: Int
get() = pagerState.targetPage
/**
* interactionSource
*/
@OptIn(ExperimentalFoundationApi::class)
val interactionSource: InteractionSource
get() = pagerState.interactionSource
/**
* 滚动到指定页面
*/
@OptIn(ExperimentalFoundationApi::class)
suspend fun scrollToPage(
// 指定的页码
@IntRange(from = 0) page: Int,
// 滚动偏移量
@FloatRange(from = 0.0, to = 1.0) pageOffset: Float = 0f,
) = pagerState.scrollToPage(page, pageOffset)
/**
* 动画滚动到指定页面
*/
@OptIn(ExperimentalFoundationApi::class)
suspend fun animateScrollToPage(
// 指定的页码
@IntRange(from = 0) page: Int,
// 滚动偏移量
@FloatRange(from = 0.0, to = 1.0) pageOffset: Float = 0f,
) = pagerState.animateScrollToPage(page, pageOffset)
}
/**
* 记录pager状态
*/
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun rememberImagePagerState(
// 默认显示的页码
@IntRange(from = 0) initialPage: Int = 0,
pageCount: () -> Int,
): ImagePagerState {
val pageState = rememberPagerState(initialPage = initialPage, pageCount = pageCount)
return remember {
ImagePagerState(pageState)
}
}
/**
* pager组件
*/
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ImageHorizonPager(
// 编辑参数
modifier: Modifier = Modifier,
// pager状态
state: ImagePagerState,
// 每个item之间的间隔
itemSpacing: Dp = 0.dp,
// 页面内容
content: @Composable (page: Int) -> Unit,
) {
HorizontalPager(
state = state.pagerState,
modifier = modifier,
pageSpacing = itemSpacing,
) { page ->
content(page)
}
}
================================================
FILE: app/mobile/src/main/kotlin/com/origeek/imageViewer/previewer/ImagePreviewer.kt
================================================
package com.origeek.imageViewer.previewer
import androidx.annotation.IntRange
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.LoadingIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.mapSaver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.origeek.imageViewer.gallery.GalleryGestureScope
import com.origeek.imageViewer.gallery.ImageGallery
import com.origeek.imageViewer.gallery.ImageGalleryState
import com.origeek.imageViewer.gallery.rememberImageGalleryState
import com.origeek.imageViewer.viewer.ImageViewerState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
// 预览的默认的背景颜色
val DEEP_DARK_FANTASY = Color(0xFF000000)
// 图片间的默认间隔
val DEFAULT_ITEM_SPACE = 12.dp
// 比较轻柔的动画窗格
val DEFAULT_SOFT_ANIMATION_SPEC = tween<Float>(320)
/**
* 预览的默认背景
*/
@Composable
fun DefaultPreviewerBackground() {
Box(
modifier = Modifier
.background(DEEP_DARK_FANTASY)
.fillMaxSize()
)
}
/**
* 预览组件的状态
*/
class ImagePreviewerState(
// 协程作用域
scope: CoroutineScope = MainScope(),
// 默认动画窗格
defaultAnimationSpec: AnimationSpec<Float> = DEFAULT_SOFT_ANIMATION_SPEC,
// 预览状态
galleryState: ImageGalleryState,
) : PreviewerVerticalDragState(scope, defaultAnimationSpec, galleryState = galleryState) {
companion object {
fun getSaver(galleryState: ImageGalleryState): Saver<ImagePreviewerState, *> {
return mapSaver(
save = {
mapOf<String, Any>(
it::currentPage.name to it.currentPage,
it::animateContainerVisibleState.name to it.animateContainerVisibleState.currentState,
it::uiAlpha.name to it.uiAlpha.value,
it::visible.name to it.visible,
)
},
restore = {
val previewerState = ImagePreviewerState(galleryState = galleryState)
previewerState.animateContainerVisibleState =
MutableTransitionState(it[ImagePreviewerState::animateContainerVisibleState.name] as Boolean)
previewerState.uiAlpha =
Animatable(it[ImagePreviewerState::uiAlpha.name] as Float)
previewerState.visible = it[ImagePreviewerState::visible.name] as Boolean
previewerState
}
)
}
}
}
/**
* 记录预览组件状态
*/
@Composable
fun rememberPreviewerState(
// 协程作用域
scope: CoroutineScope = rememberCoroutineScope(),
// 动画窗格
animationSpec: AnimationSpec<Float> = DEFAULT_SOFT_ANIMATION_SPEC,
// 开启垂直手势的类型
verticalDragType: VerticalDragType = VerticalDragType.None,
// 初始页码
@IntRange(from = 0) initialPage: Int = 0,
// 获取页数
pageCount: () -> Int,
// 提供给组件用于获取key的方法
getKey: ((Int) -> Any)? = null,
): ImagePreviewerState {
val galleryState = rememberImageGalleryState(initialPage, pageCount)
val imagePreviewerState = rememberSaveable(saver = ImagePreviewerState.getSaver(galleryState)) {
ImagePreviewerState(galleryState = galleryState)
}
imagePreviewerState.scope = scope
imagePreviewerState.getKey = getKey
imagePreviewerState.defaultAnimationSpec = animationSpec
imagePreviewerState.verticalDragType = verticalDragType
return imagePreviewerState
}
/**
* 默认的弹出预览时的动画效果
*/
val DEFAULT_PREVIEWER_ENTER_TRANSITION =
scaleIn(tween(180)) + fadeIn(tween(240))
/**
* 默认的关闭预览时的动画效果
*/
val DEFAULT_PREVIEWER_EXIT_TRANSITION =
scaleOut(tween(320)) + fadeOut(tween(240))
// 默认淡入淡出动画窗格
val DEFAULT_CROSS_FADE_ANIMATE_SPEC: AnimationSpec<Float> = tween(80)
// 加载占位默认的进入动画
val DEFAULT_PLACEHOLDER_ENTER_TRANSITION = fadeIn(tween(200))
// 加载占位默认的退出动画
val DEFAULT_PLACEHOLDER_EXIT_TRANSITION = fadeOut(tween(200))
// 默认的加载占位
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
val DEFAULT_PREVIEWER_PLACEHOLDER_CONTENT = @Composable {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
LoadingIndicator()
//CircularProgressIndicator(color = Color.White.copy(0.2F))
}
}
// 加载时的占位内容
class PreviewerPlaceholder(
// 进入动画
var enterTransition: EnterTransition = DEFAULT_PLACEHOLDER_ENTER_TRANSITION,
// 退出动画
var exitTransition: ExitTransition = DEFAULT_PLACEHOLDER_EXIT_TRANSITION,
// 占位的内容
var content: @Composable () -> Unit = DEFAULT_PREVIEWER_PLACEHOLDER_CONTENT,
)
/**
* 预览图层对象
*/
class PreviewerLayerScope(
// 包裹viewer的容器图层
var viewerContainer: @Composable (
page: Int, viewerState: ImageViewerState, viewer: @Composable () -> Unit
) -> Unit = { _, _, viewer -> viewer() },
// 背景图层
var background: @Composable ((page: Int) -> Unit) = { _ -> DefaultPreviewerBackground() },
// 前景图层
var foreground: @Composable ((page: Int) -> Unit) = { _ -> },
// 加载时的占位内容
var placeholder: PreviewerPlaceholder = PreviewerPlaceholder()
)
/**
* 图片预览组件
*/
@Composable
fun ImagePreviewer(
// 编辑参数
modifier: Modifier = Modifier,
// 状态对象
state: ImagePreviewerState,
// 图片加载器
imageLoader: @Composable (Int) -> Any?,
// 图片间的间隔
itemSpacing: Dp = DEFAULT_ITEM_SPACE,
// 进入动画
enter: EnterTransition = DEFAULT_PREVIEWER_ENTER_TRANSITION,
// 退出动画
exit: ExitTransition = DEFAULT_PREVIEWER_EXIT_TRANSITION,
// 检测手势
detectGesture: GalleryGestureScope.() -> Unit = {},
// 自定义previewer的各个图层
previewerLayer: PreviewerLayerScope.() -> Unit = {},
) {
state.apply {
// 图层相关
val layerScope = remember { PreviewerLayerScope() }
previewerLayer.invoke(layerScope)
LaunchedEffect(
key1 = animateContainerVisibleState,
key2 = animateContainerVisibleState.currentState
) {
onAnimateContainerStateChanged()
}
AnimatedVisibility(
modifier = Modifier.fillMaxSize(),
visibleState = animateContainerVisibleState,
enter = enterTransition ?: enter,
exit = exitTransition ?: exit,
) {
Box(
modifier = Modifier
.fillMaxSize()
.pointerInput(getKey) {
verticalDrag(this)
}
) {
@Composable
fun UIContainer(content: @Composable () -> Unit) {
Box(
modifier = Modifier
.fillMaxSize()
.alpha(uiAlpha.value)
) {
content()
}
}
ImageGallery(
modifier = modifier.fillMaxSize(),
state = galleryState,
imageLoader = imageLoader,
itemSpacing = itemSpacing,
detectGesture = detectGesture,
galleryLayer = {
this.viewerContainer = { page, viewerState, viewer ->
layerScope.viewerContainer(page, viewerState) {
val viewerContainerState = rememberViewerContainerState(
viewerState = viewerState,
animationSpec = defaultAnimationSpec
)
LaunchedEffect(key1 = currentPage) {
if (currentPage == page) {
state.viewerContainerState = viewerContainerState
}
}
ImageViewerContainer(
modifier = Modifier.alpha(viewerAlpha.value),
containerState = viewerContainerState,
placeholder = layerScope.placeholder,
viewer = viewer,
)
}
}
this.background = {
UIContainer {
layerScope.background(it)
}
}
this.foreground = {
UIContainer {
layerScope.foreground(it)
}
}
},
)
if (!visible)
Box(
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) { detectTapGestures { } }) { }
}
}
ticket.Next()
}
}
================================================
FILE: app/mobile/src/main/kotlin/com/origeek/imageViewer/previewer/ImageTransform.kt
================================================
package com.origeek.imageViewer.previewer
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.listSaver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.geometry.isSpecified
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.IntSize
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
/**
* @program: ImageViewer
*
* @description:
*
* @author: JVZIYAOYAO
*
* @create: 2022-09-22 10:13
**/
// 用于操作transformItemStateMap的锁对象
internal val imageTransformMutex = Mutex()
// 用于缓存界面上的transformItemState
internal val transformItemStateMap = mutableStateMapOf<Any, TransformItemState>()
@Composable
fun TransformImageView(
modifier: Modifier = Modifier,
painter: Painter,
key: Any,
itemState: TransformItemState = rememberTransformItemState(),
previewerState: ImagePreviewerState,
) {
TransformImageView(
modifier = modifier,
key = key,
itemState = itemState,
contentState = previewerState.transformState,
) { itemKey ->
key(itemKey) {
LaunchedEffect(key1 = painter.intrinsicSize) {
if (painter.intrinsicSize.isSpecified) {
itemState.intrinsicSize = painter.intrinsicSize
}
}
Image(
modifier = Modifier.fillMaxSize(),
painter = painter,
contentDescription = null,
contentScale = ContentScale.Crop,
)
}
}
}
@Composable
fun TransformImageView(
modifier: Modifier = Modifier,
bitmap: ImageBitmap,
key: Any,
itemState: TransformItemState = rememberTransformItemState(),
previewerState: ImagePreviewerState,
) {
TransformImageView(
modifier = modifier,
key = key,
itemState = itemState,
previewerState = previewerState,
) {
itemState.intrinsicSize = Size(
bitmap.width.toFloat(),
bitmap.height.toFloat()
)
Image(
modifier = Modifier.fillMaxSize(),
bitmap = bitmap,
contentDescription = null,
contentScale = ContentScale.Crop,
)
}
}
@Composable
fun TransformImageView(
modifier: Modifier = Modifier,
imageVector: ImageVector,
key: Any,
itemState: TransformItemState = rememberTransformItemState(),
previewerState: ImagePreviewerState,
) {
TransformImageView(
modifier = modifier,
key = key,
itemState = itemState,
previewerState = previewerState,
) {
LocalDensity.current.run {
itemState.intrinsicSize = Size(
imageVector.defaultWidth.toPx(),
imageVector.defaultHeight.toPx(),
)
}
Image(
modifier = Modifier.fillMaxSize(),
imageVector = imageVector,
contentDescription = null,
contentScale = ContentScale.Crop,
)
}
}
@Composable
fun TransformImageView(
modifier: Modifier = Modifier,
key: Any,
itemState: TransformItemState = rememberTransformItemState(),
previewerState: ImagePreviewerState,
content: @Composable (Any) -> Unit,
) = TransformImageView(modifier, key, itemState, previewerState.transformState, content)
@Composable
fun TransformImageView(
modifier: Modifier = Modifier,
key: Any,
itemState: TransformItemState = rememberTransformItemState(),
contentState: TransformContentState? = rememberTransformContentState(),
content: @Composable (Any) -> Unit,
) {
TransformItemView(
modifier = modifier,
key = key,
itemState = itemState,
contentState = contentState,
) {
content(key)
}
}
@Composable
fun TransformItemView(
modifier: Modifier = Modifier,
key: Any,
itemState: TransformItemState = rememberTransformItemState(),
contentState: TransformContentState?,
content: @Composable (Any) -> Unit,
) {
val scope = rememberCoroutineScope()
itemState.key = key
itemState.blockCompose = content
DisposableEffect(key) {
// 这个composable加载时添加到map
scope.launch {
itemState.addItem()
}
onDispose {
// composable退出时从map移除
itemState.removeItem()
}
}
Box(
modifier = modifier
.onGloballyPositioned {
itemState.onPositionChange(
position = it.positionInRoot(),
size = it.size,
)
}
.fillMaxSize()
) {
if (
contentState?.itemState != itemState || !contentState.onAction
) {
itemState.blockCompose(key)
}
}
}
@Composable
fun TransformContentView(
transformContentState: TransformContentState = rememberTransformContentState(),
) {
Box(
modifier = Modifier
.fillMaxSize()
.onGloballyPositioned {
transformContentState.containerSize = it.size
transformContentState.containerOffset = it.positionInRoot()
},
) {
if (
transformContentState.srcCompose != null
&& transformContentState.onAction
) {
Box(
modifier = Modifier
.offset(
x = LocalDensity.current.run { (transformContentState.offsetX.value).toDp() },
y = LocalDensity.current.run { (transformContentState.offsetY.value).toDp() },
)
.size(
width = LocalDensity.current.run { transformContentState.displayWidth.value.toDp() },
height = LocalDensity.current.run { transformContentState.displayHeight.value.toDp() },
)
.graphicsLayer {
transformOrigin = TransformOrigin(0F, 0F)
scaleX = transformContentState.graphicScaleX.value
scaleY = transformContentState.graphicScaleY.value
},
) {
transformContentState.srcCompose!!(transformContentState.itemState?.key ?: Unit)
}
}
}
}
class TransformContentState(
// 协程作用域
var scope: CoroutineScope = MainScope(),
// 默认动画窗格
var defaultAnimationSpec: AnimationSpec<Float> = DEFAULT_SOFT_ANIMATION_SPEC
) {
var itemState: TransformItemState? by mutableStateOf(null)
val intrinsicSize: Size
get() = itemState?.intrinsicSize ?: Size.Zero
val intrinsicRatio: Float
get() {
if (intrinsicSize.height == 0F) return 1F
return intrinsicSize.width.div(intrinsicSize.height)
}
val srcPosition: Offset
get() {
val offset = itemState?.blockPosition ?: Offset.Zero
return offset.copy(x = offset.x - containerOffset.x, y = offset.y - containerOffset.y)
}
val srcSize: IntSize
get() = itemState?.blockSize ?: IntSize.Zero
val srcCompose: (@Composable (Any) -> Unit)?
get() = itemState?.blockCompose
var onAction by mutableStateOf(false)
var onActionTarget by mutableStateOf<Boolean?>(null)
var displayWidth = Animatable(0F)
var displayHeight = Animatable(0F)
var graphicScaleX = Animatable(1F)
var graphicScaleY = Animatable(1F)
var offsetX = Animatable(0F)
var offsetY = Animatable(0F)
var containerOffset by mutableStateOf(Offset.Zero)
private var containerSizeState = mutableStateOf(IntSize.Zero)
var containerSize: IntSize
get() = containerSizeState.value
set(value) {
containerSizeState.value = value
if (value.width != 0 && value.height != 0) {
scope.launch {
specifierSizeFlow.emit(true)
}
}
}
var specifierSizeFlow = MutableStateFlow(false)
val containerRatio: Float
get() {
if (containerSize.height == 0) return 1F
return containerSize.width.toFloat().div(containerSize.height)
}
val widthFixed: Boolean
get() = intrinsicRatio > containerRatio
val fitSize: Size
get() {
return if (intrinsicRatio > containerRatio) {
// 宽度一致
val uW = containerSize.width
val uH = uW / intrinsicRatio
Size(uW.toFloat(), uH)
} else {
// 高度一致
val uH = containerSize.height
val uW = uH * intrinsicRatio
Size(uW, uH.toFloat())
}
}
val fitOffsetX: Float
get() {
return (containerSize.width - fitSize.width).div(2)
}
val fitOffsetY: Float
get() {
return (containerSize.height - fitSize.height).div(2)
}
val fitScale: Float
get() {
return fitSize.width.div(displayRatioSize.width)
}
val displayRatioSize: Size
get() {
return Size(width = srcSize.width.toFloat(), height = srcSize.width.div(intrinsicRatio))
}
val realSize: Size
get() {
return Size(
width = displayWidth.value * graphicScaleX.value,
height = displayHeight.value * graphicScaleY.value,
)
}
suspend fun awaitContainerSizeSpecifier() {
specifierSizeFlow.takeWhile { !it }.collect {}
}
fun findTransformItem(key: Any) = transformItemStateMap[key]
fun clearTransformItems() = transformItemStateMap.clear()
fun setEnterState() {
onAction = true
onActionTarget = null
}
fun setExitState() {
onAction = false
onActionTarget = null
}
suspend fun notifyEnterChanged() {
scope.launch {
listOf(
scope.async {
displayWidth.snapTo(displayRatioSize.width)
},
scope.async {
displayHeight.snapTo(displayRatioSize.height)
},
scope.async {
graphicScaleX.snapTo(fitScale)
},
scope.async {
graphicScaleY.snapTo(fitScale)
},
scope.async {
offsetX.snapTo(fitOffsetX)
},
scope.async {
offsetY.snapTo(fitOffsetY)
},
).awaitAll()
}
}
suspend fun exitTransform(
animationSpec: AnimationSpec<Float>? = null
) = suspendCoroutine<Unit> { c ->
val currentAnimateSpec = animationSpec ?: defaultAnimationSpec
scope.launch {
listOf(
scope.async {
displayWidth.animateTo(srcSize.width.toFloat(), currentAnimateSpec)
},
scope.async {
displayHeight.animateTo(srcSize.height.toFloat(), currentAnimateSpec)
},
scope.async {
graphicScaleX.animateTo(1F, currentAnimateSpec)
},
scope.async {
graphicScaleY.animateTo(1F, currentAnimateSpec)
},
scope.async {
offsetX.animateTo(srcPosition.x, currentAnimateSpec)
},
scope.async {
offsetY.animateTo(srcPosition.y, currentAnimateSpec)
},
).awaitAll()
onAction = false
onActionTarget = null
c.resume(Unit)
}
}
suspend fun enterTransform(
itemState: TransformItemState,
animationSpec: AnimationSpec<Float>? = null
) = suspendCoroutine<Unit> { c ->
val currentAnimationSpec = animationSpec ?: defaultAnimationSpec
this.itemState = itemState
displayWidth = Animatable(srcSize.width.toFloat())
displayHeight = Animatable(srcSize.height.toFloat())
graphicScaleX = Animatable(1F)
graphicScaleY = Animatable(1F)
offsetX = Animatable(srcPosition.x)
offsetY = Animatable(srcPosition.y)
onActionTarget = true
onAction = true
scope.launch {
reset(currentAnimationSpec)
c.resume(Unit)
onActionTarget = null
}
}
suspend fun reset(animationSpec: AnimationSpec<Float>? = null) {
val currentAnimationSpec = animationSpec ?: defaultAnimationSpec
listOf(
scope.async {
displayWidth.animateTo(displayRatioSize.width, currentAnimationSpec)
},
scope.async {
displayHeight.animateTo(displayRatioSize.height, currentAnimationSpec)
},
scope.async {
graphicScaleX.animateTo(fitScale, currentAnimationSpec)
},
scope.async {
graphicScaleY.animateTo(fitScale, currentAnimationSpec)
},
scope.async {
offsetX.animateTo(fitOffsetX, currentAnimationSpec)
},
scope.async {
offsetY.animateTo(fitOffsetY, currentAnimationSpec)
},
).awaitAll()
}
companion object {
val Saver: Saver<TransformContentState, *> = listSaver(
save = {
listOf<Any>(
it.onAction,
)
},
restore = {
val transformContentState = TransformContentState()
transformContentState.onAction = it[0] as Boolean
transformContentState
}
)
}
}
@Composable
fun rememberTransformContentState(
scope: CoroutineScope = rememberCoroutineScope(),
animationSpec: AnimationSpec<Float> = DEFAULT_SOFT_ANIMATION_SPEC
): TransformContentState {
val transformContentState = rememberSaveable(saver = TransformContentState.Saver) {
TransformContentState()
}
transformContentState.scope = scope
transformContentState.defaultAnimationSpec = animationSpec
return transformContentState
}
class TransformItemState(
var key: Any = Unit,
var blockCompose: (@Composable (Any) -> Unit) = {},
var scope: CoroutineScope,
var blockPosition: Offset = Offset.Zero,
var blockSize: IntSize = IntSize.Zero,
var intrinsicSize: Size? = null,
var checkInBound: (TransformItemState.() -> Boolean)? = null,
) {
private fun checkItemInMap() {
if (checkInBound == null) return
if (checkInBound!!.invoke(this)) {
addItem()
} else {
removeItem()
}
}
/**
* 位置和大小发生变化时
* @param position Offset
* @param size IntSize
*/
internal fun onPositionChange(position: Offset, size: IntSize) {
blockPosition = position
blockSize = size
scope.launch {
checkItemInMap()
}
}
/**
* 判断item是否在所需范围内,返回true,则添加该item到map,返回false则移除
* @param checkInBound Function0<Boolean>
*/
fun checkIfInBound(checkInBound: () -> Boolean) {
if (checkInBound()) {
addItem()
} else {
removeItem()
}
}
/**
* 添加item到map上
* @param key Any?
*/
fun addItem(key: Any? = null) {
val currentKey = key ?: this.key ?: return
if (checkInBound != null) return
synchronized(imageTransformMutex) {
transformItemStateMap[currentKey] = this
}
}
/**
* 从map上移除item
* @param key Any?
*/
fun removeItem(key: Any? = null) {
synchronized(imageTransformMutex) {
val currentKey = key ?: this.key ?: return
if (checkInBound != null) return
transformItemStateMap.remove(currentKey)
}
}
}
@Composable
fun rememberTransformItemState(
scope: CoroutineScope = rememberCoroutineScope(),
checkInBound: (TransformItemState.() -> Boolean)? = null,
): TransformItemState {
return remember { TransformItemState(scope = scope, checkInBound = checkInBound) }
}
================================================
FILE: app/mobile/src/main/kotlin/com/origeek/imageViewer/previewer/ImageViewerContainer.kt
================================================
package com.origeek.imageViewer.previewer
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.mapSaver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.unit.IntSize
import com.origeek.imageViewer.viewer.ImageViewerState
import com.origeek.imageViewer.viewer.rememberViewerState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.withContext
/**
* @program: ImageViewer
*
* @description:
*
* @author: JVZIYAOYAO
*
* @create: 2022-10-17 14:45
**/
internal class ViewerContainerState(
// 协程作用域
var scope: CoroutineScope = MainScope(),
// 转换图层的状态
var transformState: TransformContentState = TransformContentState(),
// viewer的状态
var imageViewerState: ImageViewerState = ImageViewerState(),
// 默认动画窗格
var defaultAnimationSpec: AnimationSpec<Float> = DEFAULT_SOFT_ANIMATION_SPEC
) {
/**
* +-------------------+
* INTERNAL
* +-------------------+
*/
// 转换图层transformContent透明度
internal var transformContentAlpha = Animatable(0F)
// viewer容器的透明度
internal var viewerContainerAlpha = Animatable(1F)
// 是否允许界面显示loading
internal var allowLoading by mutableStateOf(true)
// 打开图片后到加载成功过程的协程任务
internal var openTransformJob: Deferred<Unit>? = null
/**
* 取消打开动作
*/
internal fun cancelOpenTransform() {
openTransformJob?.cancel()
openTransformJob = null
}
/**
* 等待挂载成功
*/
internal suspend fun awaitOpenTransform() {
// 这里需要等待viewer挂载,显示loading界面
openTransformJob = scope.async {
// 等待viewer加载
awaitViewerLoading()
// viewer加载成功后显示viewer
transformSnapToViewer(true)
}
openTransformJob?.await()
openTransformJob = null
}
/**
* 等待viewer挂载成功
*/
internal suspend fun awaitViewerLoading() {
imageViewerState.mountedFlow.apply {
withContext(Dispatchers.Default) {
takeWhile { !it }.collect()
}
}
}
/**
* 转换图层转viewer图层,true显示viewer,false显示转换图层
* @param isViewer Boolean
*/
internal suspend fun transformSnapToViewer(isViewer: Boolean) {
if (isViewer) {
transformContentAlpha.snapTo(0F)
viewerContainerAlpha.snapTo(1F)
} else {
transformContentAlpha.snapTo(1F)
viewerContainerAlpha.snapTo(0F)
}
}
/**
* 将viewer容器的位置大小复制给transformContent
*/
internal suspend fun copyViewerContainerStateToTransformState() {
transformState.apply {
val targetScale = scale.value * fitScale
graphicScaleX.snapTo(targetScale)
graphicScaleY.snapTo(targetScale)
val centerOffsetY = (containerSize.height - realSize.height).div(2)
val centerOffsetX = (containerSize.width - realSize.width).div(2)
offsetY.snapTo(centerOffsetY + this@ViewerContainerState.offsetY.value)
offsetX.snapTo(centerOffsetX + this@ViewerContainerState.offsetX.value)
}
}
/**
* 将viewer的位置大小等信息复制给transformContent
* @param itemState TransformItemState
*/
internal suspend fun copyViewerPosToContent(itemState: TransformItemState) {
transformState.apply {
// 更新itemState,确保itemState一致
this@apply.itemState = itemState
// 确保viewer的容器大小与transform的容器大小一致
containerSize = imageViewerState.containerSize
val scale = imageViewerState.scale
val offsetX = imageViewerState.offsetX
val offsetY = imageViewerState.offsetY
// 计算transform的实际大小
val rw = fitSize.width * scale.value
val rh = fitSize.height * scale.value
// 计算目标平移量
val goOffsetX =
(containerSize.width - rw).div(2) + offsetX.value
val goOffsetY =
(containerSize.height - rh).div(2) + offsetY.value
// 计算缩放率
val fixScale = fitScale * scale.value
// 更新值
graphicScaleX.snapTo(fixScale)
graphicScaleY.snapTo(fixScale)
displayWidth.snapTo(displayRatioSize.width)
displayHeight.snapTo(displayRatioSize.height)
this@apply.offsetX.snapTo(goOffsetX)
this@apply.offsetY.snapTo(goOffsetY)
}
}
// 容器大小
var containerSize: IntSize by mutableStateOf(IntSize.Zero)
// 容器的偏移量X
var offsetX = Animatable(0F)
// 容器的偏移量Y
var offsetY = Animatable(0F)
// 容器缩放
var scale = Animatable(1F)
/**
* 重置回原来的状态
* @param animationSpec AnimationSpec<Float>
*/
suspend fun reset(animationSpec: AnimationSpec<Float> = defaultAnimationSpec) {
scope.apply {
listOf(
async {
offsetX.animateTo(0F, animationSpec)
},
async {
offsetY.animateTo(0F, animationSpec)
},
async {
scale.animateTo(1F, animationSpec)
},
).awaitAll()
}
}
/**
* 立刻重置
*/
suspend fun resetImmediately() {
offsetX.snapTo(0F)
offsetY.snapTo(0F)
scale.snapTo(1F)
}
companion object {
val Saver: Saver<ViewerContainerState, *> = mapSaver(
save = {
mapOf<String, Any>(
it::offsetX.name to it.offsetX.value,
it::offsetY.name to it.offsetY.value,
it::scale.name to it.scale.value,
)
},
restore = {
val viewerContainerState = ViewerContainerState()
viewerContainerState.offsetX =
Animatable(it[viewerContainerState::offsetX.name] as Float)
viewerContainerState.offsetY =
Animatable(it[viewerContainerState::offsetY.name] as Float)
viewerContainerState.scale =
Animatable(it[viewerContainerState::scale.name] as Float)
viewerContainerState
}
)
}
}
/**
* 记录Viewer容器的状态
* @return ViewerContainerState
*/
@Composable
internal fun rememberViewerContainerState(
// 协程作用域
scope: CoroutineScope = rememberCoroutineScope(),
// viewer状态
viewerState: ImageViewerState = rememberViewerState(),
// 转换content的state
transformContentState: TransformContentState = rememberTransformContentState(),
// 动画窗格
animationSpec: AnimationSpec<Float> = DEFAULT_SOFT_ANIMATION_SPEC,
): ViewerContainerState {
val viewerContainerState = rememberSaveable(saver = ViewerContainerState.Saver) {
ViewerContainerState()
}
viewerContainerState.scope = scope
viewerContainerState.imageViewerState = viewerState
viewerContainerState.transformState = transformContentState
viewerContainerState.defaultAnimationSpec = animationSpec
return viewerContainerState
}
/**
* Viewer容器
*/
@Composable
internal fun ImageViewerContainer(
// 修改对象
modifier: Modifier = Modifier,
// 容器状态
containerState: ViewerContainerState,
// 未加载成功时的占位
placeholder: PreviewerPlaceholder = PreviewerPlaceholder(),
// viewer主体
viewer: @Composable () -> Unit,
) {
containerState.apply {
Box(
modifier = modifier
.fillMaxSize()
.onGloballyPositioned {
containerSize = it.size
}
.graphicsLayer {
scaleX = scale.value
scaleY = scale.value
translationX = offsetX.value
translationY = offsetY.value
}
) {
// 支持转换效果的图层
Box(
modifier = Modifier
.fillMaxSize()
.alpha(transformContentAlpha.value)
) {
TransformContentView(transformState)
}
// viewer图层
Box(
modifier = Modifier
.fillMaxSize()
.alpha(viewerContainerAlpha.value)
) {
viewer()
}
// 判断viewer是否加载成功
val viewerMounted by imageViewerState.mountedFlow.collectAsState(
initial = false
)
if (allowLoading) AnimatedVisibility(
visible = !viewerMounted,
enter = placeholder.enterTransition,
exit = placeholder.exitTransition,
) {
placeholder.content()
}
}
}
}
================================================
FILE: app/mobile/src/main/kotlin/com/origeek/imageViewer/previewer/PreviewerPagerState.kt
================================================
package com.origeek.imageViewer.previewer
import androidx.annotation.FloatRange
import androidx.annotation.IntRange
import com.origeek.imageViewer.gallery.ImageGalleryState
/**
* @program: ImageViewer
*
* @description:
*
* @author: JVZIYAOYAO
*
* @create: 2022-10-17 14:41
**/
open class PreviewerPagerState(
val galleryState: ImageGalleryState,
) {
/**
* 当前页码
*/
val currentPage: Int
get() = galleryState.currentPage
/**
* 目标页码
*/
val targetPage: Int
get() = galleryState.targetPage
/**
* 滚动到指定页面
* @param page Int
* @param pageOffset Float
*/
suspend fun scrollToPage(
@IntRange(from = 0) page: Int,
@FloatRange(from = 0.0, to = 1.0) pageOffset: Float = 0F,
) = galleryState.scrollToPage(page, pageOffset)
/**
* 带动画滚动到指定页面
* @param page Int
* @param pageOffset Float
*/
suspend fun animateScrollToPage(
@IntRange(from = 0) page: Int,
@FloatRange(from = 0.0, to = 1.0) pageOffset: Float = 0F,
) = galleryState.animateScrollToPage(page, pageOffset)
}
================================================
FILE: app/mobile/src/main/kotlin/com/origeek/imageViewer/previewer/PreviewerTransformState.kt
================================================
package com.origeek.imageViewer.previewer
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.tween
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.origeek.imageViewer.gallery.ImageGalleryState
import com.origeek.imageViewer.util.Ticket
import com.origeek.imageViewer.viewer.ImageViewerState
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
/**
* @program: ImageViewer
*
* @description:
*
* @author: JVZIYAOYAO
*
* @create: 2022-10-17 14:41
**/
open class PreviewerTransformState(
// 协程作用域
var scope: CoroutineScope = MainScope(),
// 默认动画窗格
var defaultAnimationSpec: AnimationSpec<Float> = DEFAULT_SOFT_ANIMATION_SPEC,
// 预览状态
galleryState: ImageGalleryState,
) : PreviewerPagerState(
galleryState = galleryState,
) {
/**
* +-------------------+
* PRIVATE
* +-------------------+
*/
// 锁对象
private var mutex = Mutex()
// 打开回调,最外层animateVisible修改时调用
private var openCallback: (() -> Unit)? = null
// 关闭回调,最外层animateVisible修改时调用
private var closeCallback: (() -> Unit)? = null
// 是否显示viewer容器的标识
private val viewerContainerVisible: Boolean
get() = viewerContainerState?.viewerContainerAlpha?.value == 1F
/**
* 更新当前的标记状态
* @param animating Boolean
* @param visible Boolean
* @param visibleTarget Boolean?
*/
private suspend fun updateState(animating: Boolean, visible: Boolean, visibleTarget: Boolean?) {
mutex.withLock {
this.animating = animating
this.visible = visible
this.visibleTarget = visibleTarget
}
}
/**
* +-------------------+
* INTERNAL
* +-------------------+
*/
// 等待界面刷新的ticket
internal val ticket = Ticket()
// 最外侧animateVisibleState
internal var animateContainerVisibleState by mutableStateOf(MutableTransitionState(false))
// UI透明度
internal var uiAlpha = Animatable(0F)
// viewer透明度
internal var viewerAlpha = Animatable(1F)
// 从外部传入viewer容器
internal var viewerContainerState by mutableStateOf<ViewerContainerState?>(null)
// 从外部提供transformContentState
internal val transformState: TransformContentState?
get() = viewerContainerState?.transformState
// 进入转换动画
internal var enterTransition: EnterTransition? = null
// 离开的转换动画
internal var exitTransition: ExitTransition? = null
// 判断是否允许transform结束
internal val canTransformOut: Boolean
get() = (viewerContainerState?.openTransformJob != null) || (imageViewerState?.mountedFlow?.value == true)
// 标记打开动作,执行开始
internal suspend fun stateOpenStart() =
updateState(animating = true, visible = false, visibleTarget = true)
// 标记打开动作,执行结束
internal suspend fun stateOpenEnd() =
updateState(animating = false, visible = true, visibleTarget = null)
// 标记关闭动作,执行开始
internal suspend fun stateCloseStart() =
updateState(animating = true, visible = true, visibleTarget = false)
// 标记关闭动作,执行结束
internal suspend fun stateCloseEnd() =
updateState(animating = false, visible = false, visibleTarget = null)
/**
* 转换图层转viewer图层,true显示viewer,false显示转换图层
* @param isViewer Boolean
*/
internal suspend fun transformSnapToViewer(isViewer: Boolean) {
if (isViewer && visibleTarget == false) return
viewerContainerState?.transformSnapToViewer(isViewer)
}
/**
* animateVisable执行完成后调用回调方法
*/
internal fun onAnimateContainerStateChanged() {
if (animateContainerVisibleState.currentState) {
openCallback?.invoke()
transformState?.setEnterState()
} else {
closeCallback?.invoke()
}
}
/**
* +-------------------+
* PUBLIC
* +-------------------+
*/
// 是否正在进行动画
var animating by mutableStateOf(false)
internal set
// 是否可见
var visible by mutableStateOf(false)
internal set
// 是否可见的目标值
var visibleTarget by mutableStateOf<Boolean?>(null)
internal set
// 是否允许执行open操作
val canOpen: Boolean
get() = !visible && visibleTarget == null && !animating
// 是否允许执行close操作
val canClose: Boolean
get() = visible && visibleTarget == null && !animating
// imageViewer状态对象
val imageViewerState: ImageViewerState?
get() = viewerContainerState?.imageViewerState
/**
* 根据页面获取当前页码所属的key
*/
var getKey: ((Int) -> Any)? = null
// 查找key关联的transformItem
fun findTransformItem(key: Any): TransformItemState? {
return transformItemStateMap[key]
}
// 根据index查询key
fun findTransformItemByIndex(index: Int): TransformItemState? {
val key = getKey?.invoke(index) ?: return null
return findTransformItem(key)
}
// 清除全部transformItems
fun clearTransformItems() = transformItemStateMap.clear()
/**
* 打开previewer
* @param index Int
* @param itemState TransformItemState?
* @param enterTransition EnterTransition?
*/
suspend fun open(
index: Int = 0,
itemState: TransformItemState? = null,
enterTransition: EnterTransition? = null
) =
suspendCoroutine<Unit> { c ->
// 设置当前转换动画
this.enterTransition = enterTransition
// 设置转换回调
openCallback = {
c.resume(Unit)
// 清除转换回调
openCallback = null
// 清除转换动画
this.enterTransition = null
// 标记结束
scope.launch {
stateOpenEnd()
}
}
scope.launch {
// 标记开始
stateOpenStart()
// 开启UI
uiAlpha.snapTo(1F)
// container动画立即设置为关闭
animateContainerVisibleState = MutableTransitionState(false)
// 开启container
animateContainerVisibleState.targetState = true
// 跳转到index
galleryState.scrollToPage(index)
// 等待下一帧之后viewerContainerState才会刷新出来
ticket.awaitNextTicket()
// 允许显示loading
viewerContainerState?.allowLoading = true
// 开启viewer
viewerContainerState?.viewerContainerAlpha?.snapTo(1F)
// 如果输入itemState,则用itemState做为背景
if (itemState != null) {
scope.launch {
viewerContainerState?.transformContentAlpha?.snapTo(1F)
transformState?.awaitContainerSizeSpecifier()
transformState?.enterTransform(itemState, tween(0))
}
}
// 这里需要等待viewer挂载,显示loading界面
viewerContainerState?.awaitOpenTransform()
}
}
/**
* 关闭previewer
* @param exitTransition ExitTransition?
*/
suspend fun close(exitTransition: ExitTransition? = null) = suspendCoroutine<Unit> { c ->
// 设置当前退出动画
this.exitTransition = exitTransition
// 设置退出结束的回调方法
closeCallback = {
c.resume(Unit)
// 将回调设置为空
closeCallback = null
// 将退出动画设置为空
this.exitTransition = null
// 标记结束
scope.launch {
stateCloseEnd()
}
}
scope.launch {
// 标记开始
stateCloseStart()
// 关闭正在进行的开启操作
viewerContainerState?.cancelOpenTransform()
// 这里创建一个全新的state是为了让exitTransition的设置得到响应
animateContainerVisibleState = MutableTransitionState(true)
// 开启container关闭动画
animateContainerVisibleState.targetState = false
// 等待下一帧
ticket.awaitNextTicket()
// transformState标记退出
transformState?.setExitState()
}
}
/**
* 打开previewer,带转换效果
* @param index Int
* @param itemState TransformItemState
* @param animationSpec AnimationSpec<Float>?
*/
suspend fun openTransform(
index: Int,
itemState: TransformItemState? = findTransformItemByIndex(index),
animationSpec: AnimationSpec<Float> = defaultAnimationSpec
) {
// 如果itemState为空,改用open的方式打开
if (itemState == null) {
open(index)
return
}
// 动画开始
stateOpenStart()
// 关闭UI
uiAlpha.snapTo(0F)
// 关闭viewer
viewerAlpha.snapTo(0F)
// 设置新的container状态立刻设置为true
animateContainerVisibleState = MutableTransitionState(true)
// 跳转到index页
galleryState.scrollToPage(index)
// 等待下一帧
ticket.awaitNextTicket()
// 关闭loading
viewerContainerState?.allowLoading = false
// 关闭viewer。打开transform
transformSnapToViewer(false)
// 开启viewer
viewerAlpha.snapTo(1F)
// 这两个一起执行
listOf(
scope.async {
// 开启动画
transformState?.enterTransform(itemState, animationSpec)
// 开启loading
viewerContainerState?.allowLoading = true
},
scope.async {
// UI慢慢显示
uiAlpha.animateTo(1F, animationSpec)
}
).awaitAll()
// 执行完成后的回调
stateOpenEnd()
// 这里需要等待viewer挂载,显示loading界面
viewerContainerState?.awaitOpenTransform()
}
/**
* 关闭previewer,带转换效果
* @param key Any
* @param animationSpec AnimationSpec<Float>?
*/
suspend fun closeTransform(
animationSpec: AnimationSpec<Float> = defaultAnimationSpec,
) {
// 标记开始
stateCloseStart()
// 判断当前状态是否允许transform结束
// 需要在cancel前获取该值
val canNowTransformOut = canTransformOut
// 关闭可能正在进行的open操作
viewerContainerState?.cancelOpenTransform()
// 关闭loading的显示
viewerContainerState?.allowLoading = false
// 查询item是否存在
val itemState = findTransformItemByIndex(currentPage)
// 如果存在,就transform退出,否则就普通退出
if (itemState != null && canNowTransformOut) {
// 如果viewer在显示的状态,退出时将viewer的pose复制给content
if (viewerContainerVisible) {
// 标记transform的开始状态,否则copy无效
transformState?.setEnterState()
// 更新transformState
transformState?.notifyEnterChanged()
// 等待刷新完毕
ticket.awaitNextTicket()
// 复制viewer的pos给transform
viewerContainerState?.copyViewerPosToContent(itemState)
// 切换为transform
transformSnapToViewer(false)
}
// 等待下一帧
ticket.awaitNextTicket()
listOf(
scope.async {
// transform动画退出
transformState?.exitTransform(animationSpec)
// 退出结束后隐藏content
viewerContainerState?.transformContentAlpha?.snapTo(0F)
},
scope.async {
// 动画隐藏UI
uiAlpha.animateTo(0F, animationSpec)
}
).awaitAll()
// 等待下一帧
ticket.awaitNextTicket()
// 彻底关闭container
animateContainerVisibleState = MutableTransitionState(false)
} else {
// transform标记退出
transformState?.setExitState()
// container动画退出
animateContainerVisibleState.targetState = false
}
// 允许使用loading
viewerContainerState?.allowLoading = true
// 标记结束
stateCloseEnd()
}
}
================================================
FILE: app/mobile/src/main/kotlin/com/origeek/imageViewer/previewer/PreviewerVerticalDragState.kt
================================================
package com.origeek.imageViewer.previewer
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.foundation.gestures.detectVerticalDragGestures
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.PointerInputScope
import com.origeek.imageViewer.gallery.ImageGalleryState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch
import kotlin.math.absoluteValue
/**
* @program: ImageViewer
*
* @description:
*
* @author: JVZIYAOYAO
*
* @create: 2022-10-17 14:42
**/
// 默认下拉关闭缩放阈值
const val DEFAULT_SCALE_TO_CLOSE_MIN_VALUE = 0.9F
enum class VerticalDragType {
// 不开启垂直手势
None,
// 仅开启下拉手势
Down,
// 支持上下拉手势
UpAndDown,
;
}
/**
* 增加垂直方向拖拽的能力
*/
open class PreviewerVerticalDragState(
// 协程作用域
scope: CoroutineScope = MainScope(),
// 默认动画窗格
defaultAnimationSpec: AnimationSpec<Float> = DEFAULT_SOFT_ANIMATION_SPEC,
// 开启垂直手势的类型
verticalDragType: VerticalDragType = VerticalDragType.None,
// 下拉关闭的缩小的阈值
scaleToCloseMinValue: Float = DEFAULT_SCALE_TO_CLOSE_MIN_VALUE,
// 预览状态
galleryState: ImageGalleryState,
) : PreviewerTransformState(scope, defaultAnimationSpec, galleryState) {
/**
* viewer容器缩小关闭
*/
private suspend fun viewerContainerShrinkDown() {
// 标记动作开始
stateCloseStart()
listOf(
// 缩小容器
scope.async {
viewerContainerState?.scale?.animateTo(0F, animationSpec = defaultAnimationSpec)
},
// 关闭UI
scope.async {
uiAlpha.animateTo(0F, animationSpec = defaultAnimationSpec)
}
).awaitAll()
// 等待下一帧
ticket.awaitNextTicket()
// 关闭动画组件
animateContainerVisibleState = MutableTransitionState(false)
// 等待下一帧
ticket.awaitNextTicket()
// 重置container
viewerContainerState?.reset(defaultAnimationSpec)
// 将transform标记为退出
transformState?.setExitState()
// 标记动作结束
stateCloseEnd()
}
/**
* 响应下拉关闭
*/
private suspend fun dragDownClose() {
// 刷新transform的pos
transformState?.notifyEnterChanged()
// 关闭loading
viewerContainerState?.allowLoading = false
// 等待下一帧,确保transform的pos刷新成功
ticket.awaitNextTicket()
// 将container的pos复制给transform
viewerContainerState?.copyViewerContainerStateToTransformState()
// container重置
viewerContainerState?.resetImmediately()
// 切换到transform
transformSnapToViewer(false)
// 等待下一帧
ticket.awaitNextTicket()
// 执行转换关闭
closeTransform(defaultAnimationSpec)
// 解除loading限制
viewerContainerState?.allowLoading = true
}
/**
* 设置下拉手势的方法
* @param pointerInputScope PointerInputScope
*/
internal suspend fun verticalDrag(pointerInputScope: PointerInputScope) {
pointerInputScope.apply {
// 记录开始时的位置
var vStartOffset by mutableStateOf<Offset?>(null)
// 标记是否为下拉关闭
var vOrientationDown by mutableStateOf<Boolean?>(null)
// 如果getKay不为空才开始检测手势
if (verticalDragType != VerticalDragType.None) detectVerticalDragGestures(
onDragStart = OnDragStart@{
// 如果imageViewerState不存在,无法进行下拉手势
if (imageViewerState == null) return@OnDragStart
var transformItemState: TransformItemState? = null
// 查询当前transformItem
getKey?.apply {
findTransformItem(invoke(currentPage))?.apply {
transformItemState = this
}
}
// 判断是否允许变换退出,如果允许就标记动作开始
// setExitState后,在下拉过程中,itemState不会从界面上消失
if (canTransformOut) {
transformState?.setEnterState()
} else {
transformState?.setExitState()
}
// 更新当前transformItem
transformState?.itemState = transformItemState
// 只有viewer的缩放率为1时才允许下拉手势
if (imageViewerState?.scale?.value == 1F) {
vStartOffset = it
// 进入下拉手势时禁用viewer的手势
imageViewerState?.allowGestureInput = false
}
},
onDragEnd = OnDragEnd@{
// 如果开始位置为空,就退出
if (vStartOffset == null) return@OnDragEnd
// 如果containerState为空,就退出
if (viewerContainerState == null) return@OnDragEnd
// 重置开始位置和方向
vStartOffset = null
vOrientationDown = null
// 解除viewer的手势输入限制
imageViewerState?.allowGestureInput = true
// 缩放小于阈值,执行关闭动画,大于就恢复原样
if (viewerContainerState!!.scale.value < scaleToCloseMinValue) {
scope.launch {
if (getKey != null && canTransformOut) {
val key = getKey!!.invoke(currentPage)
val transformItem = findTransformItem(key)
// 如果item在画面内,就执行变换关闭,否则缩小关闭
if (transformItem != null) {
dragDownClose()
} else {
viewerContainerShrinkDown()
}
} else {
viewerContainerShrinkDown()
}
// 结束动画后需要把关闭的UI打开
uiAlpha.snapTo(1F)
}
} else {
scope.launch {
uiAlpha.animateTo(1F, defaultAnimationSpec)
}
scope.launch {
viewerContainerState?.reset(defaultAnimationSpec)
}
}
},
onVerticalDrag = OnVerticalDrag@{ change, dragAmount ->
if (imageViewerState == null) return@OnVerticalDrag
if (viewerContainerState == null) return@OnVerticalDrag
if (vStartOffset == null) return@OnVerticalDrag
if (vOrientationDown == null) vOrientationDown = dragAmount > 0
if (vOrientationDown == true || verticalDragType == VerticalDragType.UpAndDown) {
val offsetY = change.position.y - vStartOffset!!.y
val offsetX = change.position.x - vStartOffset!!.x
val containerHeight = viewerContainerState!!.containerSize.height
val scale = (containerHeight - offsetY.absoluteValue).div(
containerHeight
)
scope.launch {
uiAlpha.snapTo(scale)
viewerContainerState?.offsetX?.snapTo(offsetX)
viewerContainerState?.offsetY?.snapTo(offsetY)
viewerContainerState?.scale?.snapTo(scale)
}
} else {
// 如果不是向上,就返还输入权,以免页面卡顿
imageViewerState?.allowGestureInput = true
}
}
)
}
}
/**
* 开启垂直手势的类型
*/
var verticalDragType by mutableStateOf(verticalDragType)
/**
* 下拉关闭的缩放的阈值,当scale小于这个值,就关闭,否则还原
*/
var scaleToCloseMinValue by mutableStateOf(scaleToCloseMinValue)
}
================================================
FILE: app/mobile/src/main/kotlin/com/origeek/imageViewer/util/Ticket.kt
================================================
package com.origeek.imageViewer.util
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import java.util.UUID
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
/**
* @program: ImageViewer
*
* @description:
*
* @author: JVZIYAOYAO
*
* @create: 2023-05-24 12:15
**/
class Ticket {
private var ticket by mutableStateOf("")
private val ticketMap = mutableMapOf<String, Continuation<Unit>>()
suspend fun awaitNextTicket() = suspendCoroutine<Unit> { c ->
ticket = UUID.randomUUID().toString()
ticketMap[ticket] = c
}
private fun clearTicket() {
ticketMap.forEach {
it.value.resume(Unit)
ticketMap.remove(it.key)
}
}
@Composable
fun Next() {
LaunchedEffect(ticket) {
clearTicket()
}
}
}
================================================
FILE: app/mobile/src/main/kotlin/com/origeek/imageViewer/viewer/ImageComposeCanvas.kt
================================================
package com.origeek.imageViewer.viewer
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.BitmapRegionDecoder
import android.graphics.Matrix
import android.graphics.Rect
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.drawscope.withTransform
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import com.origeek.imageViewer.previewer.DEFAULT_CROSS_FADE_ANIMATE_SPEC
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import java.math.BigDecimal
import java.math.RoundingMode
import java.util.concurrent.LinkedBlockingDeque
import kotlin.math.absoluteValue
import kotlin.math.ceil
data class RenderBlock(
var inBound: Boolean = false,
var inSampleSize: Int = 1,
var renderOffset: IntOffset = IntOffset.Zero,
var renderSize: IntSize = IntSize.Zero,
var sliceRect: Rect = Rect(0, 0, 0, 0),
private var bitmap: Bitmap? = null,
) {
fun release() {
bitmap?.recycle()
bitmap = null
}
fun getBitmap(): Bitmap? {
return bitmap
}
fun setBitmap(bitmap: Bitmap) {
this.bitmap = bitmap
}
}
val ROTATION_0 = 0
val ROTATION_90 = 90
val ROTATION_180 = 180
val ROTATION_270 = 270
class RotationIllegalException(msg: String = "Illegal rotation angle.") : RuntimeException(msg)
class ImageDecoder(
private val decoder: BitmapRegionDecoder,
private val rotation: Int = ROTATION_0,
private val onRelease: () -> Unit = {},
) : CoroutineScope by MainScope() {
// 解码的宽度
var decoderWidth by mutableStateOf(0)
private set
// 解码的高度
var decoderHeight by mutableStateOf(0)
private set
// 解码区块大小
var blockSize by mutableStateOf(0)
private set
// 渲染列表
var renderList: Array<Array<RenderBlock>> = emptyArray()
private set
// 解码渲染队列
val renderQueue = LinkedBlockingDeque<RenderBlock>()
// 横向方块数
private var countW = 0
// 纵向方块数
private var countH = 0
// 最长边的最大方块数
private var maxBlockCount = 0
init {
// 初始化最大方块数
setMaxBlockCount(1)
}
// 构造一个渲染方块队列
private fun getRenderBlockList(): Array<Array<RenderBlock>> {
var endX: Int
var endY: Int
var sliceStartX: Int
var sliceStartY: Int
var sliceEndX: Int
var sliceEndY: Int
return Array(countH) { column ->
sliceStartY = (column * blockSize)
endY = (column + 1) * blockSize
sliceEndY = if (endY > decoderHeight) decoderHeight else endY
Array(countW) { row ->
sliceStartX = (row * blockSize)
endX = (row + 1) * blockSize
sliceEndX = if (endX > decoderWidth) decoderWidth else endX
RenderBlock(
sliceRect = Rect(
sliceStartX,
sliceStartY,
sliceEndX,
sliceEndY,
)
)
}
}
}
// 设置最长边最大方块数
fun setMaxBlockCount(count: Int): Boolean {
if (maxBlockCount == count) return false
if (decoder.isRecycled) return false
when (rotation) {
ROTATION_0, ROTATION_180 -> {
decoderWidth = decoder.width
decoderHeight = decoder.height
}
ROTATION_90, ROTATION_270 -> {
decoderWidth = decoder.height
decoderHeight = decoder.width
}
else -> throw RotationIllegalException()
}
maxBlockCount = count
blockSize =
(decoderWidth.coerceAtLeast(decoderHeight)).toFloat().div(count).toInt()
countW = ceil(decoderWidth.toFloat().div(blockSize)).toInt()
countH = ceil(decoderHeight.toFloat().div(blockSize)).toInt()
renderList = getRenderBlockList()
return true
}
// 遍历每一个渲染方块
fun forEachBlock(action: (block: RenderBlock, column: Int, row: Int) -> Unit) {
for ((column, rows) in renderList.withIndex()) {
for ((row, block) in rows.withIndex()) {
action(block, column, row)
}
}
}
// 清除全部bitmap的引用
fun clearAllBitmap() {
forEachBlock { block, _, _ ->
block.release()
}
}
// 释放资源
fun release() {
synchronized(decoder) {
if (!decoder.isRecycled) {
// 清除渲染队列
renderQueue.clear()
// 回收资源
decoder.recycle()
// 发送一个信号停止堵塞的循环
renderQueue.putFirst(RenderBlock())
}
onRelease()
}
}
fun getRotateBitmap(bitmap: Bitmap, degree: Float): Bitmap {
val matrix = Matrix()
matrix.postRotate(degree)
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, false)
}
/**
* 解码渲染区域
*/
fun decodeRegion(inSampleSize: Int, rect: Rect): Bitmap? {
synchronized(decoder) {
return try {
val ops = BitmapFactory.Options()
ops.inSampleSize = inSampleSize
if (decoder.isRecycled) return null
return if (rotation == ROTATION_0) {
decoder.decodeRegion(rect, ops)
} else {
val newRect = when (rotation) {
ROTATION_90 -> {
val nextX1 = rect.top
val nextX2 = rect.bottom
val nextY1 = decoderWidth - rect.right
val nextY2 = decoderWidth - rect.left
Rect(nextX1, nextY1, nextX2, nextY2)
}
ROTATION_180 -> {
val nextX1 = decoderWidth - rect.right
val nextX2 = decoderWidth - rect.left
val nextY1 = decoderHeight - rect.bottom
val nextY2 = decoderHeight - rect.top
Rect(nextX1, nextY1, nextX2, nextY2)
}
ROTATION_270 -> {
val nextX1 = decoderHeight - rect.bottom
val nextX2 = decoderHeight - rect.top
val nextY1 = rect.left
val nextY2 = rect.right
Rect(nextX1, nextY1, nextX2, nextY2)
}
else -> throw RotationIllegalException()
}
val srcBitmap = decoder.decodeRegion(newRect, ops)
getRotateBitmap(bitmap = srcBitmap, rotation.toFloat())
}
} catch (e: Exception) {
e.printStackTrace()
null
}
}
}
// 开启堵塞队列的循环
fun startRenderQueue(onUpdate: () -> Unit) {
launch(Dispatchers.IO) {
try {
while (!decoder.isRecycled) {
val block = renderQueue.take()
if (decoder.isRecycled) break
val bitmap = decodeRegion(block.inSampleSize, block.sliceRect)
if (bitmap != null) block.setBitmap(bitmap)
onUpdate()
}
} catch (e: InterruptedException) {
e.printStackTrace()
}
}
}
}
@Composable
fun ImageComposeCanvas(
modifier: Modifier = Modifier,
imageDecoder: ImageDecoder,
scale: Float = DEFAULT_SCALE,
offsetX: Float = DEFAULT_OFFSET_X,
offsetY: Float = DEFAULT_OFFSET_Y,
rotation: Float = DEFAULT_ROTATION,
gesture: RawGesture = RawGesture(),
onMounted: () -> Unit = {},
onSizeChange: suspend (SizeChangeContent) -> Unit = {},
crossfadeAnimationSpec: AnimationSpec<Float> = DEFAULT_CROSS_FADE_ANIMATE_SPEC,
boundClip: Boolean = true,
debugMode: Boolean = false,
) {
val scope = rememberCoroutineScope()
// 容器大小
var bSize by remember { mutableStateOf(IntSize.Zero) }
// 容器长宽比
val bRatio by remember { derivedStateOf { bSize.width.toFloat() / bSize.height.toFloat() } }
// 原图长宽比
val oRatio by remember { derivedStateOf { imageDecoder.decoderWidth.toFloat() / imageDecoder.decoderHeight.toFloat() } }
// 是否宽度与容器大小一致
var widthFixed by remember { mutableStateOf(false) }
// 长宽是否均超出容器长宽
val superSize by remember {
derivedStateOf {
imageDecoder.decoderHeight > bSize.height && imageDecoder.decoderWidth > bSize.width
}
}
// 显示的默认大小
val uSize by remember {
derivedStateOf {
if (oRatio > bRatio) {
// 宽度一致
val uW = bSize.width
val uH = uW / oRatio
widthFixed = true
IntSize(uW, uH.toInt())
} else {
// 高度一致
val uH = bSize.height
val uW = uH * oRatio
widthFixed = false
IntSize(uW.toInt(), uH)
}
}
}
// 显示的实际大小
val rSize by remember(key1 = scale) {
derivedStateOf {
IntSize(
(uSize.width * scale).toInt(),
(uSize.height * scale).toInt()
)
}
}
// 同时监听容器和实际图片大小的变化
LaunchedEffect(key1 = bSize, key2 = rSize) {
// 获取最大缩放率
val maxScale = when {
superSize -> {
imageDecoder.decoderWidth.toFloat() / uSize.width.toFloat()
}
widthFixed -> {
bSize.height.toFloat() / uSize.height.toFloat()
}
else -> {
bSize.width.toFloat() / uSize.width.toFloat()
}
}
// 回调
onSizeChange(
SizeChangeContent(
defaultSize = uSize,
containerSize = bSize,
maxScale = maxScale
)
)
}
// 判断是否需要高画质渲染
val needRenderHeightTexture by remember(key1 = bSize) {
derivedStateOf {
// 目前策略:原图的面积大于容器面积,就要渲染高画质
BigDecimal(imageDecoder.decoderWidth)
.multiply(BigDecimal(imageDecoder.decoderHeight)) > BigDecimal(bSize.height)
.multiply(BigDecimal(bSize.width))
}
}
// 标识当前是否开启高画质渲染,如果需要高画质渲染,并且缩放大于1
val renderHeightTexture by remember(key1 = scale) { derivedStateOf { needRenderHeightTexture && scale > 1 } }
// 当前采样率
var inSampleSize by remember { mutableStateOf(1) }
// 最小图的采样率
var zeroInSampleSize by remember { mutableStateOf(8) }
// 底图的采样率
var backGroundInSample by remember { mutableStateOf(0) }
// 底图bitmap
var bitmap by remember { mutableStateOf<Bitmap?>(null) }
// 监听渲染实际大小,动态修改图片的采样率
LaunchedEffect(key1 = rSize) {
if (scale < 1F) return@LaunchedEffect
inSampleSize = calculateInSampleSize(
srcWidth = imageDecoder.decoderWidth,
reqWidth = rSize.width
)
if (scale == 1F) {
zeroInSampleSize = inSampleSize
}
}
// 根据采样率变化,实时更新底图
LaunchedEffect(key1 = zeroInSampleSize, key2 = inSampleSize, key3 = needRenderHeightTexture) {
scope.launch(Dispatchers.IO) {
// 如果不需要渲染高画质,就不需要分块渲染,直接使用当前采样率,用底图来展示
val iss = if (needRenderHeightTexture) zeroInSampleSize else inSampleSize
if (iss == backGroundInSample) return@launch
backGroundInSample = iss
bitmap = imageDecoder.decodeRegion(
iss, Rect(
0,
0,
imageDecoder.decoderWidth,
imageDecoder.decoderHeight
)
)
}
}
DisposableEffect(Unit) {
onDispose {
bitmap?.recycle()
bitmap = null
}
}
// 底图偏移量X,要确保图片在容器中居中对齐
val deltaX by remember(key1 = offsetX, key2 = bSize, key3 = rSize) {
derivedStateOf {
offsetX + (bSize.width - rSize.width).toFloat().div(2)
}
}
// 底图偏移量Y,要确保图片在容器中居中对齐
val deltaY by remember(key1 = offsetY, key2 = bSize, key3 = rSize) {
derivedStateOf {
offsetY + (bSize.height - rSize.height).toFloat().div(2)
}
}
// 计算显示区域内矩形的宽度
val rectW by remember(key1 = offsetX) {
derivedStateOf {
calcLeftSize(
bSize = bSize.width.toFloat(),
rSize = rSize.width.toFloat(),
offset = offsetX,
)
}
}
// 计算显示区域内矩形的高度
val rectH by remember(key1 = offsetY, key2 = rSize) {
derivedStateOf {
calcLeftSize(
bSize = bSize.height.toFloat(),
rSize = rSize.height.toFloat(),
offset = offsetY,
)
}
}
// 渲染可见区域的开始坐标X
val stX by remember(key1 = offsetX) {
derivedStateOf {
// 计算显示区域矩形的偏移坐标
val rectDeltaX = getRectDelta(
deltaX,
rSize.width.toFloat(),
bSize.width.toFloat(),
offsetX
)
// 偏移坐标减偏移量求出矩形在图片上的相对坐标
rectDeltaX - deltaX
}
}
// 渲染可见区域的开始坐标Y
val stY by remember(key1 = offsetY) {
derivedStateOf {
// 计算显示区域矩形的偏移坐标
val rectDeltaY = getRectDelta(
deltaY,
rSize.height.toFloat(),
bSize.height.toFloat(),
offsetY
)
// 偏移坐标减偏移量求出矩形在图片上的相对坐标
rectDeltaY - deltaY
}
}
// 开始坐标加上宽度等于结束坐标
val edX by remember(key1 = offsetX) { derivedStateOf { stX + rectW } }
// 开始坐标加上高度等于结束坐标
val edY by remember(key1 = offsetY) { derivedStateOf { stY + rectH } }
// 更新时间戳,用于通知canvas更新方块
var renderUpdateTimeStamp by remember { mutableStateOf(0L) }
// 开启解码队列的循环
LaunchedEffect(key1 = Unit) {
imageDecoder.startRenderQueue {
// 解码器解码一个,就更新一次时间戳
renderUpdateTimeStamp = System.currentTimeMillis()
}
}
// 切换到不需要高画质渲染时,需要清除解码队列,清除全部的bitmap
LaunchedEffect(key1 = renderHeightTexture) {
if (!renderHeightTexture) {
imageDecoder.renderQueue.clear()
imageDecoder.clearAllBitmap()
}
}
/**
* 更新渲染队列
*/
var calcMaxCountPending by remember { mutableStateOf(false) }
// 先前的缩放比
var previousScale by remember { mutableStateOf<Float?>(null) }
// 先前的偏移量
var previousOffset by remember { mutableStateOf<Offset?>(null) }
// 记录最长边的最大方块数
var blockDividerCount by remember { mutableStateOf(1) }
// 用来标识这个参数是否有改变
var preBlockDividerCount by remember { mutableStateOf(blockDividerCount) }
// 更新渲染方块的信息
fun updateRenderList() {
// 如果此时正在重新计算渲染方块的数目,就退出
if (calcMaxCountPending) return
// 更新的时候如果缩放和偏移量没有变化,方块数量也没变,就没有必要计算了
if (
previousOffset?.x == offsetX
&& previousOffset?.y == offsetY
&& previousScale == scale
&& preBlockDividerCount == blockDividerCount
) return
previousScale = scale
previousOffset = Offset(offsetX, offsetY)
// 计算当前渲染方块大小
val renderBlockSize =
imageDecoder.blockSize * (rSize.width.toFloat().div(imageDecoder.decoderWidth))
var tlx: Int
var tly: Int
var startX: Float
var startY: Float
var endX: Float
var endY: Float
var eh: Int
var ew: Int
var needUpdate: Boolean
var previousInBound: Boolean
var previousInSampleSize: Int
var lastX: Int?
var lastY: Int? = null
var lastXDelta: Int
var lastYDelta: Int
val insertList = ArrayList<RenderBlock>()
val removeList = ArrayList<RenderBlock>()
for ((column, list) in imageDecoder.renderList.withIndex()) {
startY = column * renderBlockSize
endY = (column + 1) * renderBlockSize
tly = (deltaY + startY).toInt()
eh = (if (endY > rSize.height) rSize.height - startY else renderBlockSize).toInt()
// 由于计算的精度问题,需要确保每一个区块都要严丝合缝
lastY?.let {
if (it < tly) {
lastYDelta = tly - it
tly = it
eh += lastYDelta
}
}
lastY = tly + eh
lastX = null
for ((row, block) in list.withIndex()) {
startX = row * renderBlockSize
tlx = (deltaX + startX).toInt()
endX = (row + 1) * renderBlockSize
ew = (if (endX > rSize.width) rSize.width - startX else renderBlockSize).toInt()
previousInSampleSize = block.inSampleSize
previousInBound = block.inBound
// 记录当前区块的采用率
block.inSampleSize = inSampleSize
// 判断区块是否在可视范围内
block.inBound = checkRectInBound(
startX, startY, endX, endY,
stX, stY, edX, edY
)
// 由于计算的精度问题,需要确保每一个区块都要严丝合缝
lastX?.let {
if (it < tlx) {
lastXDelta = tlx - it
tlx = it
ew += lastXDelta
}
}
lastX = tlx + ew
// 记录区块的实际偏移量
block.renderOffset = IntOffset(tlx, tly)
// 记录区块的实际大小
block.renderSize = IntSize(
width = ew,
height = eh,
)
// 如果参数跟之前的一样,就没有必要更新bitmap
needUpdate = previousInBound != block.inBound
|| previousInSampleSize != block.inSampleSize
if (!needUpdate) continue
if (!renderHeightTexture) continue
// 解码队列操作时是有锁的,会对性能造成影响
if (block.inBound) {
if (!imageDecoder.renderQueue.contains(block)) {
insertList.add(block)
}
} else {
removeList.add(block)
block.release()
}
}
}
scope.launch(Dispatchers.IO) {
synchronized(imageDecoder.renderQueue) {
insertList.forEach {
imageDecoder.renderQueue.putFirst(it)
}
removeList.forEach {
imageDecoder.renderQueue.remove(it)
}
}
}
}
LaunchedEffect(key1 = rSize, key2 = rectW, key3 = rectH) {
// 可视区域面积
val rectArea = BigDecimal(rectW.toDouble()).multiply(BigDecimal(rectH.toDouble()))
// 实际大小面积
val realArea = BigDecimal(rSize.width).multiply(BigDecimal(rSize.height))
// 被除数不能为0
if (realArea.toFloat() == 0F) return@LaunchedEffect
// 计算实际面积的可视率
val renderAreaPercentage =
rectArea.divide(realArea, 2, RoundingMode.HALF_EVEN).toFloat()
// 根据不同可视率,匹配合适的方块数,最大只能到8
val goBlockDividerCount = when {
renderAreaPercentage > 0.6F -> 1
renderAreaPercentage > 0.025F -> 4
else -> 8
}
// 如果没变,就不要修改
if (goBlockDividerCount == blockDividerCount) return@LaunchedEffect
preBlockDividerCount = blockDividerCount
blockDividerCount = goBlockDividerCount
scope.launch(Dispatchers.IO) {
// 清空解码队列
imageDecoder.renderQueue.clear()
// 进入修改区间
calcMaxCountPending = true
imageDecoder.setMaxBlockCount(blockDividerCount)
calcMaxCountPending = false
// 离开修改区间
// 更新一下界面
updateRenderList()
}
}
// 旋转中心
val rotationCenter by remember(key1 = offsetX, key2 = offsetY, key3 = scale) {
derivedStateOf {
val cx = deltaX + rSize.width.div(2)
val cy = deltaY + rSize.height.div(2)
Offset(cx, cy)
}
}
/**
* canvas加载成功后避免闪一下
*/
val canvasAlpha = remember { Animatable(0F) }
LaunchedEffect(key1 = bitmap) {
if (bitmap != null && bitmap!!.width > 1 && bitmap!!.height > 1) {
if (canvasAlpha.value == 0F) {
scope.launch {
canvasAlpha.animateTo(
targetValue = 1F,
animationSpec = crossfadeAnimationSpec
)
onMounted()
}
}
}
}
Canvas(
modifier = modifier
.alpha(canvasAlpha.value)
.fillMaxSize()
.graphicsLayer {
// 图片位移时会超出容器大小,需要在这个地方指定是否裁切
clip = boundClip
}
.onSizeChanged {
bSize = it
}
.pointerInput(Unit) {
detectTapGestures(onLongPress = gesture.onLongPress)
}
.pointerInput(Unit) {
detectTransformGestures(
onTap = gesture.onTap,
onDoubleTap = gesture.onDoubleTap,
gestureStart = gesture.gestureStart,
gestureEnd = gesture.gestureEnd,
onGesture = gesture.onGesture,
)
},
) {
withTransform({
rotate(degrees = rotation, pivot = rotationCenter)
}) {
if (bitmap != null) {
drawImage(
image = bitmap!!.asImageBitmap(),
dstSize = IntSize(rSize.width, rSize.height),
dstOffset = IntOffset(deltaX.toInt(), deltaY.toInt()),
)
}
// 更新渲染队列
if (renderUpdateTimeStamp >= 0) updateRenderList()
if (renderHeightTexture && !calcMaxCountPending) {
imageDecoder.forEachBlock { block, _, _ ->
block.getBitmap()?.let {
drawImage(
image = it.asImageBitmap(),
dstSize = block.renderSize,
dstOffset = block.renderOffset
)
}
}
}
// 这里会把可视区域的矩形画出来
if (debugMode) {
drawRect(
color = Color.Blue.copy(0.1F),
topLeft = Offset(deltaX + stX, deltaY + stY),
size = Size(rectW, rectH)
)
}
}
}
}
fun checkRectInBound(
stX1: Float, stY1: Float, edX1: Float, edY1: Float,
stX2: Float, stY2: Float, edX2: Float, edY2: Float,
): Boolean {
if (edY1 < stY2) return false
if (stY1 > edY2) return false
if (edX1 < stX2) return false
if (stX1 > edX2) return false
return true
}
fun getRectDelta(delta: Float, rSize: Float, bSize: Float, offset: Float): Float {
return delta + if (delta < 0) {
val direction = if (rSize > bSize) -1 else 1
(offset + (direction) * (bSize - rSize)
.div(2).absoluteValue).absoluteValue
} else 0F
}
fun calcLeftSize(bSize: Float, rSize: Float, offset: Float): Float {
return if (offset.absoluteValue > (bSize - rSize).div(2).absoluteValue) {
rSize - (offset.absoluteValue - (bSize - rSize).div(2))
} else {
rSize.coerceAtMost(bSize)
}
}
fun calculateInSampleSize(
srcWidth: Int,
reqWidth: Int,
): Int {
var inSampleSize = 1
while (true) {
val iss = inSampleSize * 2
if (srcWidth.toFloat().div(iss) < reqWidth) break
inSampleSize = iss
}
return inSampleSize
}
================================================
FILE: app/mobile/src/main/kotlin/com/origeek/imageViewer/viewer/ImageComposeOrigin.kt
================================================
package com.origeek.imageViewer.viewer
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.foundation.Image
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.isSpecified
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.IntSize
import com.origeek.imageViewer.previewer.DEFAULT_CROSS_FADE_ANIMATE_SPEC
import kotlinx.coroutines.launch
class RawGesture(
val onTap: (Offset) -> Unit = {},
val onDoubleTap: (Offset) -> Unit = {},
val onLongPress: (Offset) -> Unit = {},
val gestureStart: () -> Unit = {},
val gestureEnd: (transformOnly: Boolean) -> Unit = {},
val onGesture: (centroid: Offset, pan: Offset, zoom: Float, rotation: Float, event: PointerEvent) -> Boolean = { _, _, _, _, _ -> true },
)
data class SizeChangeContent(
val defaultSize: IntSize,
val containerSize: IntSize,
val maxScale: Float,
)
@Composable
fun ImageComposeOrigin(
modifier: Modifier = Modifier,
model: Any,
scale: Float = DEFAULT_SCALE,
offsetX: Float = DEFAULT_OFFSET_X,
offsetY: Float = DEFAULT_OFFSET_Y,
rotation: Float = DEFAULT_ROTATION,
gesture: RawGesture = RawGesture(),
onMounted: () -> Unit = {},
onSizeChange: suspend (SizeChangeContent) -> Unit = {},
crossfadeAnimationSpec: AnimationSpec<Float> = DEFAULT_CROSS_FADE_ANIMATE_SPEC,
boundClip: Boolean = true,
) {
val scope = rememberCoroutineScope()
// 容器大小
var bSize by remember { mutableStateOf(IntSize(0, 0)) }
// 容器比例
val bRatio by remember { derivedStateOf { bSize.width.toFloat() / bSize.height.toFloat() } }
// 图片原始大小
var oSize by remember { mutableStateOf(IntSize(0, 0)) }
// 图片原始比例
val oRatio by remember { derivedStateOf { oSize.width.toFloat() / oSize.height.toFloat() } }
// 是否宽度与容器大小一致
var widthFixed by remember { mutableStateOf(false) }
// 长宽是否均超出容器长宽
val superSize by remember {
derivedStateOf {
oSize.height > bSize.height && oSize.width > bSize.width
}
}
// 显示大小
val uSize by remember {
derivedStateOf {
if (oRatio > bRatio) {
// 宽度一致
val uW = bSize.width
val uH = uW / oRatio
widthFixed = true
IntSize(uW, uH.toInt())
} else {
// 高度一致
val uH = bSize.height
val uW = uH * oRatio
widthFixed = false
IntSize(uW.toInt(), uH)
}
}
}
// 图片显示的真实大小
val rSize by remember {
derivedStateOf {
IntSize(
(uSize.width * scale).toInt(),
(uSize.height * scale).toInt()
)
}
}
LaunchedEffect(key1 = oSize, key2 = bSize, key3 = rSize) {
val maxScale = when {
superSize -> {
oSize.width.toFloat() / uSize.width.toFloat()
}
widthFixed -> {
bSize.height.toFloat() / uSize.height.toFloat()
}
else -> {
bSize.width.toFloat() / uSize.width.toFloat()
}
}
onSizeChange(
SizeChangeContent(
defaultSize = uSize,
containerSize = bSize,
maxScale = maxScale
)
)
}
// 图片是否加载成功
var imageSpecified by remember { mutableStateOf(false) }
// 承载容器的透明度,主要用来控制图片加载成功后的渐变效果
val viewerAlpha = remember { Animatable(0F) }
/**
* mounted回调
*/
fun goMounted() {
scope.launch {
viewerAlpha.animateTo(1F, crossfadeAnimationSpec)
onMounted()
}
}
when (model) {
is Painter -> {
var isMounted by remember { mutableStateOf(false) }
imageSpecified = model.intrinsicSize.isSpecified
LaunchedEffect(key1 = model.intrinsicSize, block = {
if (imageSpecified) {
oSize = IntSize(
model.intrinsicSize.width.toInt(),
model.intrinsicSize.height.toInt()
)
if (!isMounted) {
isMounted = true
goMounted()
}
}
})
}
is ImageVector -> {
imageSpecified = true
LocalDensity.current.run {
oSize = IntSize(
model.defaultWidth.toPx().toInt(),
model.defaultHeight.toPx().toInt(),
)
goMounted()
}
}
is ImageBitmap -> {
imageSpecified = true
oSize = IntSize(
model.width,
model.height
)
goMounted()
}
is ComposeModel -> {
imageSpecified = true
LaunchedEffect(key1 = model.intrinsicSize, block = {
oSize = if (model.intrinsicSize == IntSize.Zero) {
bSize
} else {
model.intrinsicSize
}
})
goMounted()
}
else -> throw Exception("This model type is not supported!")
}
Box(
modifier = modifier
.fillMaxSize()
.graphicsLayer {
// 图片位移时会超出容器大小,需要在这个地方指定是否裁切
clip = boundClip
alpha = viewerAlpha.value
}
.onSizeChanged {
bSize = it
}
.pointerInput(Unit) {
detectTapGestures(onLongPress = gesture.onLongPress)
}
.pointerInput(key1 = imageSpecified) {
if (imageSpecified) detectTransformGestures(
onTap = gesture.onTap,
onDoubleTap = gesture.onDoubleTap,
gestureStart = gesture.gestureStart,
gestureEnd = gesture.gestureEnd,
onGesture = gesture.onGesture,
)
},
contentAlignment = Alignment.Center,
) {
val imageModifier = Modifier
.graphicsLayer {
if (imageSpecified) {
scaleX = scale
scaleY = scale
translationX = offsetX
translationY = offsetY
rotationZ = rotation
}
}
.size(
LocalDensity.current.run { uSize.width.toDp() },
LocalDensity.current.run { uSize.height.toDp() }
)
when (model) {
is Painter -> {
Image(
painter = model,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = imageModifier,
)
}
is ImageVector -> {
Image(
imageVector = model,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = imageModifier,
)
}
is ImageBitmap -> {
Image(
bitmap = model,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = imageModifier,
)
}
is ComposeModel -> {
Box(modifier = imageModifier, contentAlignment = Alignment.Center) {
model.PoseContent()
}
}
}
}
}
================================================
FILE: app/mobile/src/main/kotlin/com/origeek/imageViewer/viewer/ImageViewer.kt
================================================
package com.origeek.imageViewer.viewer
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.FloatExponentialDecaySpec
import androidx.compose.animation.core.SpringSpec
import androidx.compose.animation.core.generateDecayAnimationSpec
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.calculateCentroid
import androidx.compose.foundation.gestures.calculateCentroidSize
import androidx.compose.foundation.gestures.calculatePan
import androidx.compose.foundation.gestures.calculateRotation
import androidx.compose.foundation.gestures.calculateZoom
import androidx.compose.foundation.gestures.forEachGesture
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.listSaver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.PointerInputScope
import androidx.compose.ui.input.pointer.consumeAllChanges
import androidx.compose.ui.input.pointer.positionChangeConsumed
import androidx.compose.ui.input.pointer.positionChanged
import androidx.compose.ui.input.pointer.util.VelocityTracker
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.zIndex
import com.origeek.imageViewer.previewer.DEFAULT_CROSS_FADE_ANIMATE_SPEC
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.absoluteValue
// 默认X轴偏移量
const val DEFAULT_OFFSET_X = 0F
// 默认Y轴偏移量
const val DEFAULT_OFFSET_Y = 0F
// 默认缩放率
const val DEFAULT_SCALE = 1F
// 默认旋转角度
const val DEFAULT_ROTATION = 0F
// 图片最小缩放率
const val MIN_SCALE = 0.5F
// 图片最大缩放率
const val MAX_SCALE_RATE = 3.2F
// 最小手指手势间距
const val MIN_GESTURE_FINGER_DISTANCE = 200
/**
* viewer状态对象,用于记录compose组件状态
*/
class ImageViewerState(
// X轴偏移量
offsetX: Float = DEFAULT_OFFSET_X,
// Y轴偏移量
offsetY: Float = DEFAULT_OFFSET_Y,
// 缩放率
scale: Float = DEFAULT_SCALE,
// 旋转角度
rotation: Float = DEFAULT_ROTATION,
// 动画窗格
animationSpec: AnimationSpec<Float>? = null,
// 淡入淡出效果
crossfadeAnimationSpec: AnimationSpec<Float>? = null,
) : CoroutineScope by MainScope() {
// 默认动画窗格
var defaultAnimateSpec: AnimationSpec<Float> = animationSpec ?: SpringSpec()
// viewer挂载成功后显示时的动画窗格
var crossfadeAnimationSpec: AnimationSpec<Float> =
crossfadeAnimationSpec ?: DEFAULT_CROSS_FADE_ANIMATE_SPEC
// x偏移
val offsetX = Animatable(offsetX)
// y偏移
val offsetY = Animatable(offsetY)
// 放大倍率
val scale = Animatable(scale)
// 旋转
val rotation = Animatable(rotation)
// 是否允许手势输入
var allowGestureInput by mutableStateOf(true)
// 默认显示大小
var defaultSize by mutableStateOf(IntSize(0, 0))
internal set
// 容器大小
internal var containerSize by mutableStateOf(IntSize(0, 0))
// 最大缩放
internal var maxScale by mutableStateOf(1F)
// 标识是否来自saver,旋转屏幕后会变成true
internal var fromSaver = false
// 恢复的时间戳
internal var resetTimeStamp by mutableStateOf(0L)
// 挂载状态
internal val mountedFlow = MutableStateFlow(false)
/**
* 判断是否有动画正在运行
* @return Boolean
*/
internal fun isRunning(): Boolean {
return scale.isRunning
|| offsetX.isRunning
|| offsetY.isRunning
|| rotation.isRunning
}
/**
* 立即设置回初始值
*/
suspend fun resetImmediately() {
rotation.snapTo(DEFAULT_ROTATION)
offsetX.snapTo(DEFAULT_OFFSET_X)
offsetY.snapTo(DEFAULT_OFFSET_Y)
scale.snapTo(DEFAULT_SCALE)
}
/**
* 设置回初始值
*/
suspend fun reset(animationSpec: AnimationSpec<Float> = defaultAnimateSpec) {
coroutineScope {
launch {
rotation.animateTo(DEFAULT_ROTATION, animationSpec)
resetTimeStamp = System.currentTimeMillis()
}
launch {
offsetX.animateTo(DEFAULT_OFFSET_X, animationSpec)
resetTimeStamp = System.currentTimeMillis()
}
launch {
offsetY.animateTo(DEFAULT_OFFSET_Y, animationSpec)
resetTimeStamp = System.currentTimeMillis()
}
launch {
scale.animateTo(DEFAULT_SCALE, animationSpec)
resetTimeStamp = System.currentTimeMillis()
}
}
}
/**
* 放大到最大
*/
suspend fun scaleToMax(
offset: Offset,
animationSpec: AnimationSpec<Float>? = null
) {
val currentAnimateSpec = animationSpec ?: defaultAnimateSpec
// 计算x和y偏移量和范围,并确保不会在放大过程中超出范围
var bcx = (containerSize.width / 2 - offset.x) * maxScale
val boundX = getBound(defaultSize.width.toFloat() * maxScale, containerSize.width.toFloat())
bcx = limitToBound(bcx, boundX)
var bcy = (containerSize.height / 2 - offset.y) * maxScale
val boundY =
getBound(defaultSize.height.toFloat() * maxScale, containerSize.height.toFloat())
bcy = limitToBound(bcy, boundY)
// 启动
coroutineScope {
launch {
scale.animateTo(maxScale, currentAnimateSpec)
}
launch {
offsetX.animateTo(bcx, currentAnimateSpec)
}
launch {
offsetY.animateTo(bcy, currentAnimateSpec)
}
}
}
/**
* 放大或缩小
*/
suspend fun toggleScale(
offset: Offset,
animationSpec: AnimationSpec<Float> = defaultAnimateSpec
) {
// 如果不等于1,就调回1
if (scale.value != 1F) {
reset(animationSpec)
} else {
scaleToMax(offset, animationSpec)
}
}
/**
* 修正offsetX,offsetY的位置
*/
suspend fun fixToBound() {
val boundX =
getBound(defaultSize.width.toFloat() * scale.value, containerSize.width.toFloat())
val boundY =
getBound(defaultSize.height.toFloat() * scale.value, containerSize.height.toFloat())
val limitX = limitToBound(offsetX.value, boundX)
val limitY = limitToBound(offsetY.value, boundY)
offsetX.snapTo(limitX)
offsetY.snapTo(limitY)
}
companion object {
val SAVER: Saver<ImageViewerState, *> = listSaver(save = {
listOf(it.offsetX.value, it.offsetY.value, it.scale.value, it.rotation.value)
}, restore = {
val state = ImageViewerState(
offsetX = it[0],
offsetY = it[1],
scale = it[2],
rotation = it[3],
)
state.fromSaver = true
state
})
}
}
/**
* 记录viewer状态
* @return ImageViewerState 返回一个状态实例
*/
@Composable
fun rememberViewerState(
// X轴偏移量
offsetX: Float = DEFAULT_OFFSET_X,
// Y轴偏移量
offsetY: Float = DEFAULT_OFFSET_Y,
// 缩放率
scale: Float = DEFAULT_SCALE,
// 旋转
rotation: Float = DEFAULT_ROTATION,
// 动画窗格
animationSpec: AnimationSpec<Float>? = null,
// 淡入淡出效果
crossfadeAnimationSpec: AnimationSpec<Float>? = null,
): ImageViewerState = rememberSaveable(saver = ImageViewerState.SAVER) {
ImageViewerState(offsetX, offsetY, scale, rotation, animationSpec, crossfadeAnimationSpec)
}
/**
* viewer手势对象
*/
class ViewerGestureScope(
// 点击事件
var onTap: (Offset) -> Unit = {},
// 双击事件
var onDoubleTap: (Offset) -> Unit = {},
// 长按事件
var onLongPress: (Offset) -> Unit = {},
)
/**
* viewer传入的Compose数据类型参数
* @property content [@androidx.compose.runtime.Composable] [@kotlin.ExtensionFunctionType] Function1<ComposeModelScope, Unit>
* @constructor
*/
class ComposeModel(
private val content: @Composable ComposeModel.() -> Unit = {}
) {
internal var intrinsicSize by mutableStateOf(IntSize.Zero)
fun updateIntrinsicSize(size: IntSize) {
intrinsicSize = size
}
@Composable
fun PoseContent() {
content()
}
}
/**
* model支持Painter、ImageBitmap、ImageVector、ImageDecoder、ComposeModel
*/
@Composable
fun ImageViewer(
// 修改参数
modifier: Modifier = Modifier,
// 图片数据
model: Any?,
// viewer状态
state: ImageViewerState = rememberViewerState(),
// 检测手势
detectGesture: ViewerGestureScope.() -> Unit = {},
// 超出容器是否显示
boundClip: Boolean = true,
// 调试模式
debugMode: Boolean = false,
) {
val viewerGestureScope = remember { ViewerGestureScope() }
detectGesture.invoke(viewerGestureScope)
val scope = rememberCoroutineScope()
// 触摸时中心位置
var centroid by remember { mutableStateOf(Offset.Zero) }
// 减速运动动画曲线
val decay = remember {
FloatExponentialDecaySpec(2f).generateDecayAnimationSpec<Float>()
}
var velocityTracker = remember { VelocityTracker() }
// 记录触摸事件中手指的个数
var eventChangeCount by remember { mutableStateOf(0) }
// 最后一次偏移运动
var lastPan by remember { mutableStateOf(Offset.Zero) }
// 手势实时的偏移范围
var boundX by remember { mutableStateOf(0F) }
var boundY by remember { mutableStateOf(0F) }
// 最大缩放率,双击的时候会放大到这个值
var maxScale by remember { mutableStateOf(1F) }
// 最大显示缩放率,缩放率超过这个值后,手势结束了就会自动恢复到这个值
val maxDisplayScale by remember { derivedStateOf { maxScale * MAX_SCALE_RATE } }
// 目标偏移量
var desX by remember { mutableStateOf(0F) }
var desY by remember { mutableStateOf(0F) }
// 目标缩放率
var desScale by remember { mutableStateOf(1F) }
// 缩放率修改前的值
var fromScale by remember { mutableStateOf(1F) }
// 计算边界使用的缩放率
var boundScale by remember { mutableStateOf(1F) }
// 目标旋转角度
var desRotation by remember { mutableStateOf(0F) }
// 要增加的旋转角度
var rotate by remember { mutableStateOf(0F) }
// 要增加的放大倍率
var zoom by remember { mutableStateOf(1F) }
// 两个手指的距离
var fingerDistanceOffset by remember { mutableStateOf(Offset.Zero) }
// 同步des的参数,在gallery的图片切换时,缩小后仍然接收手势指令,所以需要同步缩小后的参数
fun asyncDesParams() {
desX = state.offsetX.value
desY = state.offsetY.value
desScale = state.scale.value
desRotation = state.rotation.value
}
LaunchedEffect(key1 = state.resetTimeStamp) {
asyncDesParams()
}
val gesture = remember {
RawGesture(
onTap = viewerGestureScope.onTap,
onDoubleTap = viewerGestureScope.onDoubleTap,
onLongPress = viewerGestureScope.onLongPress,
gestureStart = {
if (state.allowGestureInput) {
eventChangeCount = 0
velocityTracker = VelocityTracker()
scope.launch {
state.offsetX.stop()
state.offsetY.stop()
state.offsetX.updateBounds(null, null)
state.offsetY.updateBounds(null, null)
}
asyncDesParams()
}
},
gestureEnd = { transformOnly ->
// transformOnly记录手势事件中是否有位移,如果只是点击或双击,会返回false
// 如果正在动画中,就不要执行后续动作,如:reset指令执行时
if (transformOnly && !state.isRunning() && state.allowGestureInput) {
// 处理加速度添加的点为空的情况
var velocity = try {
velocityTracker.calculateVelocity()
} catch (e: Exception) {
e.printStackTrace()
null
}
// 如果缩放比小于1,要自动回到1
// 如果缩放比大于最大显示缩放比,就设置回去,并且避免加速度
val scale = when {
state.scale.value < 1 -> 1F
state.scale.value > maxDisplayScale -> {
velocity = null
maxDisplayScale
}
else -> null
}
// 如果此时位移超出范围,就动画回范围内
// 如果没超出范围,就设置animate的范围,然后执行抛掷动画
scope.launch {
if (inBound(state.offsetX.value, boundX) && velocity != null) {
val vx = sameDirection(lastPan.x, velocity.x)
state.offsetX.updateBounds(-boundX, boundX)
state.offsetX.animateDecay(vx, decay)
} else {
val targetX = if (scale != maxDisplayScale) {
limitToBound(state.offsetX.value, boundX)
} else {
panTransformAndScale(
offset = state.offsetX.value,
center = centroid.x,
bh = state.containerSize.width.toFloat(),
uh = state.defaultSize.width.toFloat(),
fromScale = state.scale.value,
toScale = scale,
)
}
state.offsetX.animateTo(targetX)
}
}
scope.launch {
if (inBound(state.offsetY.value, boundY) && velocity != null) {
val vy = sameDirection(lastPan.y, velocity.y)
state.offsetY.updateBounds(-boundY, boundY)
state.offsetY.animateDecay(vy, decay)
} else {
val targetY = if (scale != maxDisplayScale) {
limitToBound(state.offsetY.value, boundY)
} else {
panTransformAndScale(
offset = state.offsetY.value,
center = centroid.y,
bh = state.containerSize.height.toFloat(),
uh = state.defaultSize.height.toFloat(),
fromScale = state.scale.value,
toScale = scale,
)
}
state.offsetY.animateTo(targetY)
}
}
scope.launch {
state.rotation.animateTo(0F)
}
scale?.let {
scope.launch {
state.scale.animateTo(scale)
}
}
}
},
) { center, pan, _zoom, _rotate, event ->
// 当禁止手势输入时
if (!state.allowGestureInput) return@RawGesture true
// 这里只记录最大手指数
if (event.changes.size > eventChangeCount) eventChangeCount = event.changes.size
// 如果手指数从多个变成一个,就结束本次手势操作
if (eventChangeCount > event.changes.size) return@RawGesture false
rotate = _rotate
zoom = _zoom
// 如果是双指的情况下,手指距离小于一定值时,缩放和旋转的值会很离谱,所以在这种极端情况下就不要处理缩放和旋转了
if (event.changes.size == 2) {
fingerDistanceOffset = event.changes[0].position - event.changes[1].position
if (
fingerDistanceOffset.x.absoluteValue < MIN_GESTURE_FINGER_DISTANCE
&& fingerDistanceOffset.y.absoluteValue < MIN_GESTURE_FINGER_DISTANCE
) {
rotate = 0F
zoom = 1F
}
}
// 上一次的偏移量
lastPan = pan
// 记录手势的中点
centroid = center
// 记录当前缩放比
fromScale = desScale
// 目标放大倍率
desScale *= zoom
// 检查最小放大倍率
if (desScale < MIN_SCALE) desScale = MIN_SCALE
// 计算边界,如果目标缩放值超过最大显示缩放值,边界就要用最大缩放值来计算,否则手势结束时会导致无法归位
boundScale = if (desScale > maxDisplayScale) maxDisplayScale else desScale
boundX =
getBound(boundScale * state.defaultSize.width, state.containerSize.width.toFloat())
boundY =
getBound(
boundScale * state.defaultSize.height,
state.containerSize.height.toFloat()
)
desX = panTransformAndScale(
offset = desX,
center = center.x,
bh = state.containerSize.width.toFloat(),
uh = state.defaultSize.width.toFloat(),
fromScale = fromScale,
toScale = desScale,
) + pan.x
// 如果手指数1,就是拖拽,拖拽受范围限制
// 如果手指数大于1,即有缩放事件,则支持中心点放大
if (eventChangeCount == 1) desX = limitToBound(desX, boundX)
desY = panTransformAndScale(
offset = desY,
center = center.y,
bh = state.containerSize.height.toFloat(),
uh = state.defaultSize.height.toFloat(),
fromScale = fromScale,
toScale = desScale,
) + pan.y
if (eventChangeCount == 1) desY = limitToBound(desY, boundY)
if (desScale < 1) desRotation += rotate
velocityTracker.addPosition(
event.changes[0].uptimeMillis,
Offset(desX, desY),
)
if (!state.isRunning()) scope.launch {
state.scale.snapTo(desScale)
state.offsetX.snapTo(desX)
state.offsetY.snapTo(desY)
state.rotation.snapTo(desRotation)
}
// 这里判断是否已运动到边界,如果到了边界,就不消费事件,让上层界面获取到事件
val onLeft = desX >= boundX
val onRight = desX <= -boundX
val reachSide = !(onLeft && pan.x > 0)
&& !(onRight && pan.x < 0)
&& !(onLeft && onRight)
if (reachSide || state.scale.value < 1) {
event.changes.fastForEach {
if (it.positionChanged()) {
it.consumeAllChanges()
}
}
}
// 返回true,继续下一次手势
return@RawGesture true
}
}
val sizeChange: suspend (SizeChangeContent) -> Unit = { content ->
maxScale = content.maxScale
state.defaultSize = content.defaultSize
state.containerSize = content.containerSize
state.maxScale = content.maxScale
if (state.fromSaver) {
state.fromSaver = false
state.fixToBound()
}
}
Box(modifier = modifier) {
/**
* 将挂载信息通知到state
*/
val onMounted: () -> Unit = {
scope.launch {
state.mountedFlow.emit(true)
}
}
/**
* 根据不同类型的model进行不同的渲染
*/
when (model) {
is Painter,
is ImageVector,
is ImageBitmap,
is ComposeModel,
-> {
ImageComposeOrigin(
model = model,
scale = state.scale.value,
offsetX = state.offsetX.value,
offsetY = state.offsetY.value,
rotation = state.rotation.value,
gesture = gesture,
onSizeChange = sizeChange,
onMounted = onMounted,
boundClip = boundClip,
crossfadeAnimationSpec = state.crossfadeAnimationSpec,
)
}
is ImageDecoder -> {
ImageComposeCanvas(
imageDecoder = model,
scale = state.scale.value,
offsetX = state.offsetX.value,
offsetY = state.offsetY.value,
rotation = state.rotation.value,
gesture = gesture,
onSizeChange = sizeChange,
onMounted = onMounted,
boundClip = boundClip,
crossfadeAnimationSpec = state.crossfadeAnimationSpec,
)
}
}
/**
* 调试模式
*/
if (debugMode) {
Box(
modifier = Modifier
.fillMaxSize()
.zIndex(10F)
) {
if (model != null) {
Text(text = "Model -> ON", Modifier.background(Color.White))
}
Box(
modifier = Modifier
.graphicsLayer {
translationX = centroid.x - 6.dp.toPx()
translationY = centroid.y - 6.dp.toPx()
}
.clip(CircleShape)
.background(Color.Red.copy(0.4f))
.size(12.dp)
)
}
}
}
}
/**
* 重写事件监听方法
*/
suspend fun PointerInputScope.detectTransformGestures(
panZoomLock: Boolean = false,
gestureStart: () -> Unit = {},
gestureEnd: (Boolean) -> Unit = {},
onTap: (Offset) -> Unit = {},
onDoubleTap: (Offset) -> Unit = {},
onGesture: (centroid: Offset, pan: Offset, zoom: Float, rotation: Float, event: PointerEvent) -> Boolean,
) {
var lastReleaseTime = 0L
var scope: CoroutineScope? = null
forEachGesture {
awaitPointerEventScope {
var rotation = 0f
var zoom = 1f
var pan = Offset.Zero
var pastTouchSlop = false
val touchSlop = viewConfiguration.touchSlop
var lockedToPanZoom = false
awaitFirstDown(requireUnconsumed = false)
val t0 = System.currentTimeMillis()
var releasedEvent: PointerEvent? = null
var moveCount = 0
// 这里开始事件
gestureStart()
do {
val event = awaitPointerEvent()
if (event.type == PointerEventType.Release) releasedEvent = event
if (event.type == PointerEventType.Move) moveCount++
val canceled = event.changes.fastAny { it.positionChangeConsumed() }
if (!canceled) {
val zoomChange = event.calculateZoom()
val rotationChange = event.calculateRotation()
val panChange = event.calculatePan()
if (!pastTouchSlop) {
zoom *= zoomChange
rotation += rotationChange
pan += panChange
val centroidSize = event.calculateCentroidSize(useCurrent = false)
val zoomMotion = abs(1 - zoom) * centroidSize
val rotationMotion = abs(rotation * PI.toFloat() * centroidSize / 180f)
val panMotion = pan.getDistance()
if (zoomMotion > touchSlop ||
rotationMotion > touchSlop ||
panMotion > touchSlop
) {
pastTouchSlop = true
lockedToPanZoom = panZoomLock && rotationMotion < touchSlop
}
}
if (pastTouchSlop) {
val centroid = event.calculateCentroid(useCurrent = false)
val effectiveRotation = if (lockedToPanZoom) 0f else rotationChange
if (effectiveRotation != 0f ||
zoomChange != 1f ||
panChange != Offset.Zero
) {
if (!onGesture(
centroid,
panChange,
zoomChange,
effectiveRotation,
event
)
) break
}
}
}
} while (!canceled && event.changes.fastAny { it.pressed })
var t1 = System.currentTimeMillis()
val dt = t1 - t0
val dlt = t1 - lastReleaseTime
if (moveCount == 0) releasedEvent?.let { e ->
if (e.changes.isEmpty()) return@let
val offset = e.changes.first().position
if (dlt < 272) {
t1 = 0L
scope?.cancel()
onDoubleTap(offset)
} else if (dt < 200) {
scope = MainScope()
scope?.launch(Dispatchers.Main) {
delay(272)
onTap(offset)
}
}
lastReleaseTime = t1
}
// 这里是事件结束
gestureEnd(moveCount != 0)
}
}
}
/**
* 让后一个数与前一个数的符号保持一致
* @param a Float
* @param b Float
* @return Float
*/
fun sameDirection(a: Float, b: Float): Float {
return if (a > 0) {
if (b < 0) {
b.absoluteValue
} else {
b
}
} else {
if (b > 0) {
-b
} else {
b
}
}
}
/**
* 获取移动边界
*/
fun getBound(rw: Float, bw: Float): Float {
return if (rw > bw) {
var xb = (rw - bw).div(2)
if (xb < 0) xb = 0F
xb
} else {
0F
}
}
/**
* 判断位移是否在边界内
*/
fun inBound(offset: Float, bound: Float): Boolean {
return if (offset > 0) {
offset < bound
} else if (offset < 0) {
offset > -bound
} else {
true
}
}
/**
* 把位移限制在边界内
*/
fun limitToBound(offset: Float, bound: Float): Float {
return when {
offset > bound -> {
bound
}
offset < -bound -> {
-bound
}
else -> {
offset
}
}
}
/**
* 追踪缩放过程中的中心点
*/
fun panTransformAndScale(
offset: Float,
center: Float,
bh: Float,
uh: Float,
fromScale: Float,
toScale: Float,
): Float {
val srcH = uh * fromScale
val desH = uh * toScale
val gapH = (bh - uh) / 2
val py = when {
uh >= bh -> {
val upy = (uh * fromScale - uh).div(2)
(upy - offset + center) / (fromScale * uh)
}
srcH > bh || bh > uh -> {
val upy = (srcH - uh).div(2)
(upy - gapH - offset + center) / (fromScale * uh)
}
else -> {
val upy = -(bh - srcH).div(2)
(upy - offset + center) / (fromScale * uh)
}
}
return when {
uh >= bh -> {
val upy = (uh * toScale - uh).div(2)
upy + center - py * toScale * uh
}
desH > bh -> {
val upy = (desH - uh).div(2)
upy - gapH + center - py * toScale * uh
}
else -> {
val upy = -(bh - desH).div(2)
upy + center - py * desH
}
}
}
================================================
FILE: app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/activities/DynamicDetailActivity.kt
================================================
package dev.aaa1115910.bv.mobile.activities
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import dev.aaa1115910.bv.mobile.screen.DynamicDetailScreen
import dev.aaa1115910.bv.mobile.theme.BVMobileTheme
import io.github.oshai.kotlinlogging.KotlinLogging
class DynamicDetailActivity : ComponentActivity() {
companion object {
private val logger = KotlinLogging.logger { }
fun actionStart(context: Context, dynamicId: String) {
logger.info { "actionStart: dynamicId=$dynamicId" }
context.startActivity(
Intent(context, DynamicDetailActivity::class.java).apply {
putExtra("dynamicId", dynamicId)
}
)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BVMobileTheme {
DynamicDetailScreen()
}
}
}
}
================================================
FILE: app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/activities/FavoriteActivity.kt
================================================
package dev.aaa1115910.bv.mobile.activities
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import dev.aaa1115910.bv.mobile.screen.FavoriteScreen
import dev.aaa1115910.bv.mobile.theme.BVMobileTheme
class FavoriteActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val windowSize = calculateWindowSizeClass(this)
BVMobileTheme {
FavoriteScreen(
windowSize = windowSize
)
}
}
}
}
================================================
FILE: app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/activities/FollowingSeasonActivity.kt
================================================
package dev.aaa1115910.bv.mobile.activities
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import dev.aaa1115910.bv.mobile.screen.FollowingSeasonScreen
import dev.aaa1115910.bv.mobile.theme.BVMobileTheme
class FollowingSeasonActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val windowSize = calculateWindowSizeClass(this)
BVMobileTheme {
FollowingSeasonScreen(
windowSize = windowSize
)
}
}
}
}
================================================
FILE: app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/activities/FollowingUserActivity.kt
================================================
package dev.aaa1115910.bv.mobile.activities
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import dev.aaa1115910.bv.mobile.screen.FollowingUserScreen
import dev.aaa1115910.bv.mobile.theme.BVMobileTheme
class FollowingUserActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BVMobileTheme {
FollowingUserScreen()
}
}
}
}
================================================
FILE: app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/activities/HistoryActivity.kt
================================================
package dev.aaa1115910.bv.mobile.activities
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import dev.aaa1115910.bv.mobile.screen.HistoryScreen
import dev.aaa1115910.bv.mobile.theme.BVMobileTheme
class HistoryActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val windowSize = calculateWindowSizeClass(this)
BVMobileTheme {
HistoryScreen(
windowSize = windowSize
)
}
}
}
}
================================================
FILE: app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/activities/IntentHandlerActivity.kt
================================================
package dev.aaa1115910.bv.mobile.activities
import android.os.Bundle
import androidx.activity.ComponentActivity
import dev.aaa1115910.bv.entity.BvScheme
import io.github.oshai.kotlinlogging.KotlinLogging
class IntentHandlerActivity : ComponentActivity() {
companion object {
private val logger = KotlinLogging.logger { }
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val uri = intent.data
when (uri?.host) {
BvScheme.QrToken.HOST -> QrTokenResultActivity.launch(this, uri)
else -> {
logger.info { "unknown uri: $uri" }
finish()
}
}
}
}
================================================
FILE: app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/activities/LoginActivity.kt
================================================
package dev.aaa1115910.bv.mobile.activities
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import dev.aaa1115910.bv.mobile.screen.LoginScreen
import dev.aaa1115910.bv.mobile.theme.BVMobileTheme
class LoginActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BVMobileTheme {
LoginScreen()
}
}
}
}
================================================
FILE: app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/activities/MainActivity.kt
================================================
package dev.aaa1115910.bv.mobile.activities
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import dev.aaa1115910.bv.mobile.screen.MobileMainScreen
import dev.aaa1115910.bv.mobile.screen.RegionBlockScreen
import dev.aaa1115910.bv.mobile.theme.BVMobileTheme
import dev.aaa1115910.bv.util.NetworkUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
var keepSplashScreen = true
installSplashScreen().apply {
setKeepOnScreenCondition { keepSplashScreen }
}
super.onCreate(savedInstanceState)
setContent {
val scope = rememberCoroutineScope()
var isCheckingNetwork by remember { mutableStateOf(true) }
var isMainlandChina by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
scope.launch(Dispatchers.IO) {
isMainlandChina = false // NetworkUtil.isMainlandChina()
isCheckingNetwork = false
keepSplashScreen = false
}
}
BVMobileTheme {
if (isCheckingNetwork) {
// 避免提前加载内容
// } else if (isMainlandChina) {
// RegionBlockScreen()
} else {
MobileMainScreen()
}
}
}
}
}
================================================
FILE: app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/activities/QrTokenResultActivity.kt
================================================
package dev.aaa1115910.bv.mobile.activities
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import dev.aaa1115910.bv.mobile.screen.QrTokenResultScreen
import io.github.oshai.kotlinlogging.KotlinLogging
class QrTokenResultActivity : ComponentActivity() {
companion object {
private val logger = KotlinLogging.logger { }
fun launch(context: Context, uri: Uri) {
logger.info { "launch QrTokenResultActivity: uri=$uri" }
context.startActivity(
Intent(context, QrTokenResultActivity::class.java).apply {
putExtra("uri", uri)
}
)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
QrTokenResultScreen()
}
}
}
================================================
FILE: app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/activities/SettingsActivity.kt
================================================
package dev.aaa1115910.bv.mobile.activities
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import dev.aaa1115910.bv.mobile.screen.settings.SettingsScreen
import dev.aaa1115910.bv.mobile.theme.BVMobileTheme
class SettingsActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BVMobileTheme {
SettingsScreen()
}
}
}
}
================================================
FILE: app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/activities/UserSpaceActivity.kt
================================================
package dev.aaa1115910.bv.mobile.activities
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import dev.aaa1115910.bv.mobile.screen.UserSpaceScreen
import dev.aaa1115910.bv.mobile.theme.BVMobileTheme
class UserSpaceActivity : ComponentActivity() {
companion object {
fun actionStart(context: Context, mid: Long, name: String) {
context.startActivity(
Intent(context, UserSpaceActivity::class.java).apply {
putExtra("mid", mid)
putExtra("name", name)
}
)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BVMobileTheme {
UserSpaceScreen()
}
}
}
}
================================================
FILE: app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/activities/VideoPlayerActivity.kt
================================================
package dev.aaa1115910.bv.mobile.activities
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.lifecycle.lifecycleScope
import dev.aaa1115910.biliapi.entity.ApiType
import dev.aaa1115910.biliapi.http.BiliHttpApi
import dev.aaa1115910.bv.R
import dev.aaa1115910.bv.entity.PlayerType
import dev.aaa1115910.bv.mobile.screen.VideoPlayerScreen
import dev.aaa1115910.bv.mobile.theme.BVMobileTheme
import dev.aaa1115910.bv.player.VideoPlayerOptions
import dev.aaa1115910.bv.player.impl.exo.ExoPlayerFactory
import dev.aaa1115910.bv.util.Prefs
import dev.aaa1115910.bv.util.fInfo
import dev.aaa1115910.bv.util.toast
import dev.aaa1115910.bv.viewmodel.CommentViewModel
import dev.aaa1115910.bv.viewmodel.VideoPlayerV3ViewModel
import dev.aaa1115910.bv.viewmodel.video.VideoDetailViewModel
import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.androidx.viewmodel.ext.android.viewModel
class VideoPlayerActivity : ComponentActivity() {
companion object {
fun actionStart(
context: Context,
aid: Long,
//cid: Long,
fromSeason: Boolean = false,
epid: Int? = null,
seasonId: Int? = null,
) {
context.startActivity(
Intent(context, VideoPlayerActivity::class.java).apply {
putExtra("aid", aid)
//putExtra("cid", cid)
putExtra("fromSeason", fromSeason)
epid?.let { putExtra("epid", it) }
seasonId?.let { putExtra("seasonId", it) }
}
)
}
}
private val playerViewModel: VideoPlayerV3ViewModel by viewModel()
private val commentViewModel: CommentViewModel by viewModel()
private val videoDetailViewModel: VideoDetailViewModel by viewModel()
private val logger = KotlinLogging.logger {}
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initVideoPlayer()
setContent {
val windowSizeClass = calculateWindowSizeClass(this)
BVMobileTheme {
VideoPlayerScreen(
windowSizeClass = windowSizeClass
)
}
}
}
private fun initVideoPlayer() {
if (playerViewModel.videoPlayer != null) return
logger.fInfo { "initVideoPlayer" }
val options = VideoPlayerOptions(
userAgent = when (Prefs.apiType) {
ApiType.Web -> dev.aaa1115910.biliapi.BiliApiConstants.USER_AGENT_WEB
ApiType.App -> dev.aaa1115910.biliapi.BiliApiConstants.USER_AGENT_APP
},
referer = when (Prefs.apiType) {
ApiType.Web -> getString(R.string.video_player_referer)
ApiType.App -> null
}
)
val videoPlayer = when (Prefs.playerType) {
PlayerType.Media3 -> ExoPlayerFactory().create(this, options)
}
playerViewModel.videoPlayer = videoPlayer
//TODO 还没处理旋转后的一些判断,就先放这了
parseIntent()
}
private fun parseIntent() {
var aid = intent.getLongExtra("aid", 0)
var cid = intent.getLongExtra("cid", 0)
val fromSeason = intent.getBooleanExtra("fromSeason", false)
val epid = intent.getIntExtra("epid", 0)
val seasonId = intent.getIntExtra("seasonId", 0)
lifecycleScope.launch(Dispatchers.IO) {
if (aid == 0L && cid == 0L) {
runCatching {
val acid = BiliHttpApi.getAidCidByEpid(epid)!!
aid = acid.first
cid = acid.second
}.onFailure {
logger.fInfo { "get avid & cid by epid failed: ${it.stackTraceToString()}" }
withContext(Dispatchers.Main) {
it.message?.toast(this@VideoPlayerActivity)
}
}
}
commentViewModel.commentType = 1
commentViewModel.commentId = aid
runCatching {
videoDetailViewModel.loadDetail(aid, fromSeason)
}.onFailure {
withContext(Dispatchers.Main) {
it.message?.toast(this@VideoPlayerActivity)
}
}
runCatching {
playerViewModel.fromSeason = fromSeason
playerViewModel.loadPlayUrl(
avid = videoDetailViewModel.videoDetail?.aid ?: 0,
cid = videoDetailViewModel.videoDetail?.cid ?: 0,
epid = epid.takeIf { it != 0 },
seasonId = seasonId.takeIf { it != 0 }
)
}.onFailure {
withContext(Dispatchers.Main) {
it.message?.toast(this@VideoPlayerActivity)
}
}
}
}
override fun onDestroy() {
super.onDestroy()
if (isFinishing) {
playerViewModel.releasePlayerResources("onDestroy")
}
}
override fun onPause() {
playerViewModel.videoPlayer?.isInBackground = true
playerViewModel.videoPlayer?.pause()
super.onPause()
}
}
================================================
FILE: app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/home/SearchBar.kt
================================================
package dev.aaa1115910.bv.mobile.component.home
import android.content.Context
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.DockedSearchBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.semantics.isTraversalGroup
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.traversalIndex
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import dev.aaa1115910.bv.BVApp
import dev.aaa1115910.bv.R
import dev.aaa1115910.bv.mobile.theme.BVMobileTheme
@Composable
fun HomeSearchTopBarCompact(
modifier: Modifier = Modifier,
query: String,
expanded: Boolean,
selectedTabIndex: Int,
onQueryChange: (String) -> Unit,
onExpandedChange: (Boolean) -> Unit,
onOpenNavDrawer: () -> Unit,
onChangeTabIndex: (Int) -> Unit,
onSwitchUser: () -> Unit
) {
val context = LocalContext.current
var currentTab by remember { mutableStateOf(HomeTab.Recommend) }
val searchBarHorizontalPadding by animateDpAsState(
targetValue = if (expanded) 0.dp else 16.dp,
label = "search bar horizontal padding"
)
Box(
modifier = modifier,
contentAlignment = Alignment.TopCenter
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = searchBarHorizontalPadding)
.zIndex(2f),
horizontalArrangement = Arrangement.Center
) {
HomeSearchBar(
modifier = Modifier.fillMaxWidth(),
query = query,
expanded = expanded,
onQueryChange = onQueryChange,
onExpandedChange = onExpandedChange,
onOpenNavDrawer = onOpenNavDrawer,
onSwitchUser = onSwitchUser,
onSearch = {}
)
}
TabRow(
modifier = Modifier
.padding(top = 100.dp)
.zIndex(1f),
selectedTabIndex = selectedTabIndex
) {
HomeTab.entries.forEachIndexed { index, tab ->
Tab(
selected = selectedTabIndex == index,
onClick = {
onChangeTabIndex(index)
currentTab = tab
},
text = {
Text(
text = tab.getDisplayName(context),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
gitextract_fcqp1f9p/
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml
│ │ ├── config.yml
│ │ └── feature_request.yml
│ └── workflows/
│ ├── alpha.yml
│ ├── alpha_build_manually_without_sign.yml
│ ├── auto_close_issues.yml
│ ├── close_inactive_issues.yml
│ ├── features.yml
│ └── release.yml
├── .gitignore
├── .gitmodules
├── CHANGELOG.md
├── LICENSE
├── README.md
├── app/
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── compose_compiler_config.conf
│ ├── mobile/
│ │ ├── .gitignore
│ │ ├── build.gradle.kts
│ │ ├── consumer-rules.pro
│ │ ├── proguard-rules.pro
│ │ └── src/
│ │ └── main/
│ │ ├── AndroidManifest.xml
│ │ ├── kotlin/
│ │ │ ├── com/
│ │ │ │ └── origeek/
│ │ │ │ └── imageViewer/
│ │ │ │ ├── gallery/
│ │ │ │ │ ├── ImageGallery.kt
│ │ │ │ │ └── ImagePager.kt
│ │ │ │ ├── previewer/
│ │ │ │ │ ├── ImagePreviewer.kt
│ │ │ │ │ ├── ImageTransform.kt
│ │ │ │ │ ├── ImageViewerContainer.kt
│ │ │ │ │ ├── PreviewerPagerState.kt
│ │ │ │ │ ├── PreviewerTransformState.kt
│ │ │ │ │ └── PreviewerVerticalDragState.kt
│ │ │ │ ├── util/
│ │ │ │ │ └── Ticket.kt
│ │ │ │ └── viewer/
│ │ │ │ ├── ImageComposeCanvas.kt
│ │ │ │ ├── ImageComposeOrigin.kt
│ │ │ │ └── ImageViewer.kt
│ │ │ └── dev/
│ │ │ └── aaa1115910/
│ │ │ └── bv/
│ │ │ └── mobile/
│ │ │ ├── activities/
│ │ │ │ ├── DynamicDetailActivity.kt
│ │ │ │ ├── FavoriteActivity.kt
│ │ │ │ ├── FollowingSeasonActivity.kt
│ │ │ │ ├── FollowingUserActivity.kt
│ │ │ │ ├── HistoryActivity.kt
│ │ │ │ ├── IntentHandlerActivity.kt
│ │ │ │ ├── LoginActivity.kt
│ │ │ │ ├── MainActivity.kt
│ │ │ │ ├── QrTokenResultActivity.kt
│ │ │ │ ├── SettingsActivity.kt
│ │ │ │ ├── UserSpaceActivity.kt
│ │ │ │ └── VideoPlayerActivity.kt
│ │ │ ├── component/
│ │ │ │ ├── home/
│ │ │ │ │ ├── SearchBar.kt
│ │ │ │ │ ├── UserDialog.kt
│ │ │ │ │ └── dynamic/
│ │ │ │ │ ├── DynamicItem.kt
│ │ │ │ │ └── DynamicUserItem.kt
│ │ │ │ ├── player/
│ │ │ │ │ └── VideoPlayerPages.kt
│ │ │ │ ├── preferences/
│ │ │ │ │ ├── PreferenceGroup.kt
│ │ │ │ │ ├── PreferencesPreview.kt
│ │ │ │ │ └── items/
│ │ │ │ │ ├── BaseListItem.kt
│ │ │ │ │ ├── ListItemPreference.kt
│ │ │ │ │ ├── RadioPreference.kt
│ │ │ │ │ ├── SwitchPreference.kt
│ │ │ │ │ └── TextPreference.kt
│ │ │ │ ├── reply/
│ │ │ │ │ ├── CommentItem.kt
│ │ │ │ │ ├── Comments.kt
│ │ │ │ │ ├── Replies.kt
│ │ │ │ │ └── ReplySheetScaffold.kt
│ │ │ │ ├── search/
│ │ │ │ │ ├── PgcCard.kt
│ │ │ │ │ ├── UgcCard.kt
│ │ │ │ │ └── UserCard.kt
│ │ │ │ ├── settings/
│ │ │ │ │ └── UpdateDialog.kt
│ │ │ │ ├── user/
│ │ │ │ │ └── UserAvatar.kt
│ │ │ │ └── videocard/
│ │ │ │ ├── RelatedVideoItem.kt
│ │ │ │ ├── SeasonCard.kt
│ │ │ │ ├── SmallVideoCard.kt
│ │ │ │ ├── UpIcon.kt
│ │ │ │ └── UpSpaceVideoItem.kt
│ │ │ ├── screen/
│ │ │ │ ├── DynamicDetailScreen.kt
│ │ │ │ ├── FavoriteScreen.kt
│ │ │ │ ├── FollowingSeasonScreen.kt
│ │ │ │ ├── FollowingUserScreen.kt
│ │ │ │ ├── HistoryScreen.kt
│ │ │ │ ├── LoginScreen.kt
│ │ │ │ ├── MobileMainScreen.kt
│ │ │ │ ├── QrTokenResultScreen.kt
│ │ │ │ ├── RegionBlockScreen.kt
│ │ │ │ ├── UserSpaceScreen.kt
│ │ │ │ ├── VideoPlayerScreen.kt
│ │ │ │ ├── home/
│ │ │ │ │ ├── DynamicScreen.kt
│ │ │ │ │ ├── HomeScreen.kt
│ │ │ │ │ ├── SearchScreen.kt
│ │ │ │ │ ├── home/
│ │ │ │ │ │ ├── PopularPage.kt
│ │ │ │ │ │ └── RcmdPage.kt
│ │ │ │ │ └── search/
│ │ │ │ │ ├── SearchInput.kt
│ │ │ │ │ └── SearchResult.kt
│ │ │ │ └── settings/
│ │ │ │ ├── SettingsCategories.kt
│ │ │ │ ├── SettingsDetails.kt
│ │ │ │ ├── SettingsScreen.kt
│ │ │ │ └── details/
│ │ │ │ ├── AboutContent.kt
│ │ │ │ ├── AdvanceContent.kt
│ │ │ │ ├── DebugContent.kt
│ │ │ │ └── PlayContent.kt
│ │ │ └── theme/
│ │ │ └── Theme.kt
│ │ └── res/
│ │ └── values/
│ │ ├── strings.xml
│ │ └── themes.xml
│ ├── proguard-rules.pro
│ ├── shared/
│ │ ├── .gitignore
│ │ ├── build.gradle.kts
│ │ ├── consumer-rules.pro
│ │ ├── proguard-rules.pro
│ │ ├── schemas/
│ │ │ └── dev.aaa1115910.bv.dao.AppDatabase/
│ │ │ ├── 1.json
│ │ │ ├── 2.json
│ │ │ └── 3.json
│ │ └── src/
│ │ ├── debug/
│ │ │ ├── AndroidManifest.xml
│ │ │ └── res/
│ │ │ └── values/
│ │ │ └── strings.xml
│ │ ├── main/
│ │ │ ├── AndroidManifest.xml
│ │ │ ├── kotlin/
│ │ │ │ ├── coil/
│ │ │ │ │ └── transform/
│ │ │ │ │ └── BlurTransformation.kt
│ │ │ │ ├── de/
│ │ │ │ │ └── schnettler/
│ │ │ │ │ └── datastore/
│ │ │ │ │ └── manager/
│ │ │ │ │ ├── DataStoreManager.kt
│ │ │ │ │ └── PreferenceRequest.kt
│ │ │ │ └── dev/
│ │ │ │ └── aaa1115910/
│ │ │ │ ├── bv/
│ │ │ │ │ ├── BVApp.kt
│ │ │ │ │ ├── activities/
│ │ │ │ │ │ └── LauncherActivity.kt
│ │ │ │ │ ├── component/
│ │ │ │ │ │ ├── BvPlayerPreview.kt
│ │ │ │ │ │ ├── DevelopingTip.kt
│ │ │ │ │ │ ├── FpsMonitor.kt
│ │ │ │ │ │ ├── QrImage.kt
│ │ │ │ │ │ └── settings/
│ │ │ │ │ │ └── UpdateDialog.kt
│ │ │ │ │ ├── dao/
│ │ │ │ │ │ ├── AppDatabase.kt
│ │ │ │ │ │ ├── SearchHistoryDao.kt
│ │ │ │ │ │ └── UserDao.kt
│ │ │ │ │ ├── entity/
│ │ │ │ │ │ ├── AuthData.kt
│ │ │ │ │ │ ├── BvScheme.kt
│ │ │ │ │ │ ├── InterfaceMode.kt
│ │ │ │ │ │ ├── NavSwitchMode.kt
│ │ │ │ │ │ ├── PlayerType.kt
│ │ │ │ │ │ ├── ThemeType.kt
│ │ │ │ │ │ ├── carddata/
│ │ │ │ │ │ │ ├── SeasonCardData.kt
│ │ │ │ │ │ │ └── VideoCardData.kt
│ │ │ │ │ │ ├── db/
│ │ │ │ │ │ │ ├── SearchHistoryDB.kt
│ │ │ │ │ │ │ └── UserDB.kt
│ │ │ │ │ │ └── proxy/
│ │ │ │ │ │ └── ProxyArea.kt
│ │ │ │ │ ├── network/
│ │ │ │ │ │ ├── GithubApi.kt
│ │ │ │ │ │ ├── HttpServer.kt
│ │ │ │ │ │ ├── VlcLibsApi.kt
│ │ │ │ │ │ └── entity/
│ │ │ │ │ │ └── GithubRelease.kt
│ │ │ │ │ ├── player/
│ │ │ │ │ │ └── entity/
│ │ │ │ │ │ ├── DefaultSubtitle.kt
│ │ │ │ │ │ ├── NextVideoStrategy.kt
│ │ │ │ │ │ ├── PlayerDefaultStartPosition.kt
│ │ │ │ │ │ └── PlayerLoadNextAction.kt
│ │ │ │ │ ├── repository/
│ │ │ │ │ │ ├── UserRepository.kt
│ │ │ │ │ │ └── VideoInfoRepository.kt
│ │ │ │ │ ├── ui/
│ │ │ │ │ │ └── theme/
│ │ │ │ │ │ ├── Theme.kt
│ │ │ │ │ │ └── Typography.kt
│ │ │ │ │ ├── util/
│ │ │ │ │ │ ├── AbiUtil.kt
│ │ │ │ │ │ ├── BlacklistUtil.kt
│ │ │ │ │ │ ├── CodecUtil.kt
│ │ │ │ │ │ ├── CoilConfig.kt
│ │ │ │ │ │ ├── DanmakuRateLimiter.kt
│ │ │ │ │ │ ├── DeviceUtil.kt
│ │ │ │ │ │ ├── EnumExtends.kt
│ │ │ │ │ │ ├── Extends.kt
│ │ │ │ │ │ ├── LiveStreamUrlFetcher.kt
│ │ │ │ │ │ ├── LogCatcherUtil.kt
│ │ │ │ │ │ ├── ModifierExtends.kt
│ │ │ │ │ │ ├── NetworkUtil.kt
│ │ │ │ │ │ ├── NotYetImplemented.kt
│ │ │ │ │ │ ├── PartitionUtil.kt
│ │ │ │ │ │ ├── PgcIndexParamExtends.kt
│ │ │ │ │ │ ├── PgcTypeExtends.kt
│ │ │ │ │ │ ├── Prefs.kt
│ │ │ │ │ │ ├── UgcTypeExtends.kt
│ │ │ │ │ │ ├── UgcTypeV2Extends.kt
│ │ │ │ │ │ └── calculateWindowSizeClassInPreview.kt
│ │ │ │ │ └── viewmodel/
│ │ │ │ │ ├── CommentViewModel.kt
│ │ │ │ │ ├── DynamicDetailViewModel.kt
│ │ │ │ │ ├── SeasonViewModel.kt
│ │ │ │ │ ├── TagViewModel.kt
│ │ │ │ │ ├── UserSwitchViewModel.kt
│ │ │ │ │ ├── UserViewModel.kt
│ │ │ │ │ ├── VideoPlayerV3ViewModel.kt
│ │ │ │ │ ├── home/
│ │ │ │ │ │ ├── DynamicViewModel.kt
│ │ │ │ │ │ ├── PopularViewModel.kt
│ │ │ │ │ │ └── RecommendViewModel.kt
│ │ │ │ │ ├── index/
│ │ │ │ │ │ └── PgcIndexViewModel.kt
│ │ │ │ │ ├── live/
│ │ │ │ │ │ └── LiveViewModel.kt
│ │ │ │ │ ├── login/
│ │ │ │ │ │ ├── AppQrLoginViewModel.kt
│ │ │ │ │ │ └── SmsLoginViewModel.kt
│ │ │ │ │ ├── pgc/
│ │ │ │ │ │ ├── PgcAnimeViewModel.kt
│ │ │ │ │ │ ├── PgcDocumentaryViewModel.kt
│ │ │ │ │ │ ├── PgcGuoChuangViewModel.kt
│ │ │ │ │ │ ├── PgcMovieViewModel.kt
│ │ │ │ │ │ ├── PgcTvViewModel.kt
│ │ │ │ │ │ ├── PgcVarietyViewModel.kt
│ │ │ │ │ │ └── PgcViewModel.kt
│ │ │ │ │ ├── search/
│ │ │ │ │ │ ├── SearchInputViewModel.kt
│ │ │ │ │ │ └── SearchResultViewModel.kt
│ │ │ │ │ ├── ugc/
│ │ │ │ │ │ ├── UgcAiViewModel.kt
│ │ │ │ │ │ ├── UgcAnimalViewModel.kt
│ │ │ │ │ │ ├── UgcCarViewModel.kt
│ │ │ │ │ │ ├── UgcCinephileViewModel.kt
│ │ │ │ │ │ ├── UgcDanceViewModel.kt
│ │ │ │ │ │ ├── UgcDougaViewModel.kt
│ │ │ │ │ │ ├── UgcEmotionViewModel.kt
│ │ │ │ │ │ ├── UgcEntViewModel.kt
│ │ │ │ │ │ ├── UgcFashionViewModel.kt
│ │ │ │ │ │ ├── UgcFoodViewModel.kt
│ │ │ │ │ │ ├── UgcGameViewModel.kt
│ │ │ │ │ │ ├── UgcGymViewModel.kt
│ │ │ │ │ │ ├── UgcHandmakeViewModel.kt
│ │ │ │ │ │ ├── UgcHealthViewModel.kt
│ │ │ │ │ │ ├── UgcHomeViewModel.kt
│ │ │ │ │ │ ├── UgcInformationViewModel.kt
│ │ │ │ │ │ ├── UgcKichikuViewModel.kt
│ │ │ │ │ │ ├── UgcKnowledgeViewModel.kt
│ │ │ │ │ │ ├── UgcLifeExperienceViewModel.kt
│ │ │ │ │ │ ├── UgcLifeJoyViewModel.kt
│ │ │ │ │ │ ├── UgcMusicViewModel.kt
│ │ │ │ │ │ ├── UgcMysticismViewModel.kt
│ │ │ │ │ │ ├── UgcOutdoorsViewModel.kt
│ │ │ │ │ │ ├── UgcPaintingViewModel.kt
│ │ │ │ │ │ ├── UgcParentingViewModel.kt
│ │ │ │ │ │ ├── UgcRuralViewModel.kt
│ │ │ │ │ │ ├── UgcShortplayViewModel.kt
│ │ │ │ │ │ ├── UgcSportsViewModel.kt
│ │ │ │ │ │ ├── UgcTechViewModel.kt
│ │ │ │ │ │ ├── UgcTravelViewModel.kt
│ │ │ │ │ │ ├── UgcViewModel.kt
│ │ │ │ │ │ └── UgcVlogViewModel.kt
│ │ │ │ │ ├── user/
│ │ │ │ │ │ ├── FavoriteViewModel.kt
│ │ │ │ │ │ ├── FollowViewModel.kt
│ │ │ │ │ │ ├── FollowingSeasonViewModel.kt
│ │ │ │ │ │ ├── HistoryViewModel.kt
│ │ │ │ │ │ ├── ToViewViewModel.kt
│ │ │ │ │ │ └── UserSpaceViewModel.kt
│ │ │ │ │ └── video/
│ │ │ │ │ └── VideoDetailViewModel.kt
│ │ │ │ └── m3qrcode/
│ │ │ │ ├── DampedString.kt
│ │ │ │ ├── EmphasizedInterpolator.kt
│ │ │ │ ├── EntryAnimationStyle.kt
│ │ │ │ ├── MaterialShapeQr.kt
│ │ │ │ ├── MaterialShapeQrErrorCorrectionLevel.kt
│ │ │ │ ├── MaterialShapeQrState.kt
│ │ │ │ └── MaterialShapeRenderer.kt
│ │ │ ├── proto/
│ │ │ │ └── blacklist.proto
│ │ │ └── res/
│ │ │ ├── drawable/
│ │ │ │ ├── ic_banner_foreground.xml
│ │ │ │ ├── ic_banner_foreground_2.xml
│ │ │ │ ├── ic_danmaku_count.xml
│ │ │ │ ├── ic_gamer_ani.xml
│ │ │ │ ├── ic_launcher_foreground.xml
│ │ │ │ ├── ic_launcher_foreground_2.xml
│ │ │ │ ├── ic_play_count.xml
│ │ │ │ ├── ic_up.xml
│ │ │ │ ├── qrcode_hor_bar_s2_capsule.xml
│ │ │ │ ├── qrcode_hor_bar_s2_half_capsule.xml
│ │ │ │ ├── qrcode_hor_bar_s3_capsule.xml
│ │ │ │ ├── qrcode_hor_bar_s3_half_capsule.xml
│ │ │ │ ├── qrcode_square_s1_circle.xml
│ │ │ │ ├── qrcode_square_s1_drop.xml
│ │ │ │ ├── qrcode_square_s1_semi_circle.xml
│ │ │ │ ├── qrcode_square_s1_square.xml
│ │ │ │ ├── qrcode_square_s2_circle.xml
│ │ │ │ ├── qrcode_square_s2_clover.xml
│ │ │ │ ├── qrcode_square_s2_hexagonal.xml
│ │ │ │ ├── qrcode_square_s2_meteroid.xml
│ │ │ │ ├── qrcode_square_s2_wiggle_star.xml
│ │ │ │ ├── qrcode_square_s3_circle.xml
│ │ │ │ ├── qrcode_square_s3_clover.xml
│ │ │ │ ├── qrcode_square_s3_hexagonal.xml
│ │ │ │ ├── qrcode_square_s3_meteroid.xml
│ │ │ │ ├── qrcode_square_s3_wiggle_star.xml
│ │ │ │ ├── qrcode_square_s7_ring.xml
│ │ │ │ ├── qrcode_ver_bar_s2_capsule.xml
│ │ │ │ └── qrcode_ver_bar_s3_capsule.xml
│ │ │ ├── mipmap-anydpi-v26/
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── raw/
│ │ │ │ ├── ic_playing.json
│ │ │ │ └── lottie_qrcode_background.json
│ │ │ ├── values/
│ │ │ │ ├── arrays.xml
│ │ │ │ ├── colors.xml
│ │ │ │ ├── strings.xml
│ │ │ │ └── themes.xml
│ │ │ └── xml/
│ │ │ ├── network_security_config.xml
│ │ │ └── provider_paths.xml
│ │ ├── r8Test/
│ │ │ ├── AndroidManifest.xml
│ │ │ └── res/
│ │ │ └── values/
│ │ │ └── strings.xml
│ │ └── test/
│ │ └── kotlin/
│ │ ├── android/
│ │ │ └── util/
│ │ │ └── Log.kt
│ │ └── dev/
│ │ └── aaa1115910/
│ │ └── bv/
│ │ └── network/
│ │ └── GithubApiTest.kt
│ ├── src/
│ │ └── main/
│ │ └── AndroidManifest.xml
│ └── tv/
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── consumer-rules.pro
│ ├── proguard-rules.pro
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ ├── kotlin/
│ │ └── dev/
│ │ └── aaa1115910/
│ │ └── bv/
│ │ └── tv/
│ │ ├── activities/
│ │ │ ├── MainActivity.kt
│ │ │ ├── pgc/
│ │ │ │ ├── PgcIndexActivity.kt
│ │ │ │ └── anime/
│ │ │ │ └── AnimeTimelineActivity.kt
│ │ │ ├── search/
│ │ │ │ ├── SearchInputActivity.kt
│ │ │ │ └── SearchResultActivity.kt
│ │ │ ├── settings/
│ │ │ │ ├── LogsActivity.kt
│ │ │ │ ├── MediaCodecActivity.kt
│ │ │ │ ├── SettingsActivity.kt
│ │ │ │ └── SpeedTestActivity.kt
│ │ │ ├── user/
│ │ │ │ ├── FavoriteActivity.kt
│ │ │ │ ├── FollowActivity.kt
│ │ │ │ ├── FollowingSeasonActivity.kt
│ │ │ │ ├── HistoryActivity.kt
│ │ │ │ ├── LoginActivity.kt
│ │ │ │ ├── ToViewActivity.kt
│ │ │ │ ├── UserInfoActivity.kt
│ │ │ │ ├── UserLockSettingsActivity.kt
│ │ │ │ └── UserSwitchActivity.kt
│ │ │ └── video/
│ │ │ ├── RemoteControllerPanelDemoActivity.kt
│ │ │ ├── SeasonInfoActivity.kt
│ │ │ ├── TagActivity.kt
│ │ │ ├── UpInfoActivity.kt
│ │ │ ├── VideoInfoActivity.kt
│ │ │ └── VideoPlayerV3Activity.kt
│ │ ├── component/
│ │ │ ├── Carousel.kt
│ │ │ ├── CommentItem.kt
│ │ │ ├── CommentPanel.kt
│ │ │ ├── DescriptionPanel.kt
│ │ │ ├── FullscreenImageViewer.kt
│ │ │ ├── GeetestTvVerifyDialog.kt
│ │ │ ├── LibVLCDownloaderDialog.kt
│ │ │ ├── LoadingTip.kt
│ │ │ ├── RemoteControlPanelDemo.kt
│ │ │ ├── SubCommentItem.kt
│ │ │ ├── SubCommentPanel.kt
│ │ │ ├── TopNav.kt
│ │ │ ├── TvAlertDialog.kt
│ │ │ ├── UpIcon.kt
│ │ │ ├── UserPanel.kt
│ │ │ ├── buttons/
│ │ │ │ ├── CoinButton.kt
│ │ │ │ ├── FavoriteButton.kt
│ │ │ │ ├── LikeButton.kt
│ │ │ │ ├── SeasonInfoButtons.kt
│ │ │ │ └── ToViewButton.kt
│ │ │ ├── live/
│ │ │ │ └── LiveRoomCard.kt
│ │ │ ├── pgc/
│ │ │ │ └── IndexFilter.kt
│ │ │ ├── search/
│ │ │ │ ├── SearchKeyword.kt
│ │ │ │ └── SoftKeyboard.kt
│ │ │ ├── settings/
│ │ │ │ ├── SettingListItem.kt
│ │ │ │ ├── SettingNumberListItem.kt
│ │ │ │ ├── SettingSwitchListItem.kt
│ │ │ │ ├── SettingsMenuSelectItem.kt
│ │ │ │ └── UpdateDialog.kt
│ │ │ └── videocard/
│ │ │ ├── LargeVideoCard.kt
│ │ │ ├── SeasonCard.kt
│ │ │ ├── SmallVideoCard.kt
│ │ │ ├── TabbedVideosPanel.kt
│ │ │ └── VideosRow.kt
│ │ ├── manager/
│ │ │ ├── FollowStateManager.kt
│ │ │ ├── PlayedAidsCache.kt
│ │ │ └── VideoUserActionManager.kt
│ │ ├── screens/
│ │ │ ├── MainScreen.kt
│ │ │ ├── RegionBlockScreen.kt
│ │ │ ├── SeasonInfoScreen.kt
│ │ │ ├── TagScreen.kt
│ │ │ ├── VideoInfoScreen.kt
│ │ │ ├── VideoPlayerV3Screen.kt
│ │ │ ├── login/
│ │ │ │ ├── AppQRLoginContent.kt
│ │ │ │ ├── LoginScreen.kt
│ │ │ │ └── SmsLoginContent.kt
│ │ │ ├── main/
│ │ │ │ ├── DrawerContent.kt
│ │ │ │ ├── HomeContent.kt
│ │ │ │ ├── LiveContent.kt
│ │ │ │ ├── PgcContent.kt
│ │ │ │ ├── UgcContent.kt
│ │ │ │ ├── home/
│ │ │ │ │ ├── DynamicsScreen.kt
│ │ │ │ │ ├── PopularScreen.kt
│ │ │ │ │ └── RecommendScreen.kt
│ │ │ │ ├── pgc/
│ │ │ │ │ ├── AnimeContent.kt
│ │ │ │ │ ├── DocumentaryContent.kt
│ │ │ │ │ ├── GuoChuangContent.kt
│ │ │ │ │ ├── MovieContent.kt
│ │ │ │ │ ├── PgcCommon.kt
│ │ │ │ │ ├── PgcIndexScreen.kt
│ │ │ │ │ ├── TvContent.kt
│ │ │ │ │ ├── VarietyContent.kt
│ │ │ │ │ └── anime/
│ │ │ │ │ └── AnimeTimelineScreen.kt
│ │ │ │ └── ugc/
│ │ │ │ ├── UgcChildRegionButtons.kt
│ │ │ │ ├── UgcCommon.kt
│ │ │ │ ├── UgcContentFactory.kt
│ │ │ │ └── UgcStateManager.kt
│ │ │ ├── search/
│ │ │ │ ├── SearchInputScreen.kt
│ │ │ │ ├── SearchResultFilter.kt
│ │ │ │ └── SearchResultScreen.kt
│ │ │ ├── settings/
│ │ │ │ ├── LogsScreen.kt
│ │ │ │ ├── MediaCodecScreen.kt
│ │ │ │ ├── SettingsScreen.kt
│ │ │ │ ├── SpeedTestScreen.kt
│ │ │ │ └── content/
│ │ │ │ ├── AboutSetting.kt
│ │ │ │ ├── InfoSetting.kt
│ │ │ │ ├── NetworkSetting.kt
│ │ │ │ ├── OtherSetting.kt
│ │ │ │ ├── PlayerSetting.kt
│ │ │ │ ├── PlayerTypeSetting.kt
│ │ │ │ ├── StorageSetting.kt
│ │ │ │ └── UISetting.kt
│ │ │ └── user/
│ │ │ ├── FavoriteScreen.kt
│ │ │ ├── FollowScreen.kt
│ │ │ ├── FollowingSeasonFilter.kt
│ │ │ ├── FollowingSeasonScreen.kt
│ │ │ ├── HistoryScreen.kt
│ │ │ ├── ToViewScreen.kt
│ │ │ ├── UpInfoScreen.kt
│ │ │ ├── UserInfoScreen.kt
│ │ │ ├── UserSwitchScreen.kt
│ │ │ └── lock/
│ │ │ ├── UnlockSwitchUserContent.kt
│ │ │ ├── UnlockUserScreen.kt
│ │ │ └── UserLockSettingsScreen.kt
│ │ └── util/
│ │ ├── NavItemsExtensions.kt
│ │ ├── PlayerActivityUtil.kt
│ │ ├── ProvideListBringIntoViewSpec.kt
│ │ └── TvLazyListFocusRestorer.kt
│ └── res/
│ └── values/
│ ├── dimens.xml
│ ├── strings.xml
│ └── themes.xml
├── bili-api/
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── example-response/
│ │ └── live-event/
│ │ ├── COMBO_SEND.json5
│ │ ├── DANMU_MSG.json5
│ │ ├── ENTRY_EFFECT.json5
│ │ ├── GUARD_BUY.json5
│ │ ├── HOT_RANK_CHANGED.json5
│ │ ├── HOT_RANK_CHANGED_V2.json5
│ │ ├── HOT_RANK_SETTLEMENT.json5
│ │ ├── HOT_RANK_SETTLEMENT_V2.json5
│ │ ├── HOT_ROOM_NOTIFY.json5
│ │ ├── INTERACT_WORD.json5
│ │ ├── LIKE_INFO_V3_CLICK.json5
│ │ ├── LIKE_INFO_V3_UPDATE.json5
│ │ ├── LIVE_INTERACTIVE_GAME.json5
│ │ ├── LIVE_MULTI_VIEW_CHANGE.json5
│ │ ├── NOTICE_MSG.json5
│ │ ├── ONLINE_RANK_COUNT.json5
│ │ ├── ONLINE_RANK_TOP3.json5
│ │ ├── ONLINE_RANK_V2.json5
│ │ ├── PREPARING.json5
│ │ ├── ROOM_REAL_TIME_MESSAGE_UPDATE.json
│ │ ├── SEND_GIFT.json
│ │ ├── STOP_LIVE_ROOM_LIST.json5
│ │ ├── SUPER_CHAT_ENTRANCE.json5
│ │ ├── SUPER_CHAT_MESSAGE.json5
│ │ ├── SUPER_CHAT_MESSAGE_JPN.json5
│ │ ├── USER_TOAST_MSG.json5
│ │ ├── WATCHED_CHANGE.json5
│ │ └── WIDGET_BANNER.json5
│ ├── grpc/
│ │ ├── .gitignore
│ │ ├── build.gradle.kts
│ │ └── proto/
│ │ ├── bilibili/
│ │ │ ├── account/
│ │ │ │ └── fission/
│ │ │ │ └── v1/
│ │ │ │ └── fission.proto
│ │ │ ├── ad/
│ │ │ │ └── v1/
│ │ │ │ └── ad.proto
│ │ │ ├── api/
│ │ │ │ ├── player/
│ │ │ │ │ └── v1/
│ │ │ │ │ └── player.proto
│ │ │ │ ├── probe/
│ │ │ │ │ └── v1/
│ │ │ │ │ └── probe.proto
│ │ │ │ └── ticket/
│ │ │ │ └── v1/
│ │ │ │ └── ticket.proto
│ │ │ ├── app/
│ │ │ │ ├── archive/
│ │ │ │ │ ├── middleware/
│ │ │ │ │ │ └── v1/
│ │ │ │ │ │ └── preload.proto
│ │ │ │ │ └── v1/
│ │ │ │ │ └── archive.proto
│ │ │ │ ├── card/
│ │ │ │ │ └── v1/
│ │ │ │ │ ├── ad.proto
│ │ │ │ │ ├── card.proto
│ │ │ │ │ ├── common.proto
│ │ │ │ │ ├── double.proto
│ │ │ │ │ └── single.proto
│ │ │ │ ├── click/
│ │ │ │ │ └── v1/
│ │ │ │ │ └── heartbeat.proto
│ │ │ │ ├── distribution/
│ │ │ │ │ ├── setting/
│ │ │ │ │ │ ├── download.proto
│ │ │ │ │ │ ├── dynamic.proto
│ │ │ │ │ │ ├── experimental.proto
│ │ │ │ │ │ ├── internaldevice.proto
│ │ │ │ │ │ ├── night.proto
│ │ │ │ │ │ ├── other.proto
│ │ │ │ │ │ ├── pegasus.proto
│ │ │ │ │ │ ├── play.proto
│ │ │ │ │ │ ├── privacy.proto
│ │ │ │ │ │ └── search.proto
│ │ │ │ │ └── v1/
│ │ │ │ │ └── distribution.proto
│ │ │ │ ├── dynamic/
│ │ │ │ │ ├── common/
│ │ │ │ │ │ └── dynamic.proto
│ │ │ │ │ ├── v1/
│ │ │ │ │ │ └── dynamic.proto
│ │ │ │ │ └── v2/
│ │ │ │ │ ├── campus.proto
│ │ │ │ │ ├── dynamic.proto
│ │ │ │ │ └── opus.proto
│ │ │ │ ├── interfaces/
│ │ │ │ │ └── v1/
│ │ │ │ │ ├── history.proto
│ │ │ │ │ ├── media.proto
│ │ │ │ │ ├── search.proto
│ │ │ │ │ └── space.proto
│ │ │ │ ├── listener/
│ │ │ │ │ └── v1/
│ │ │ │ │ └── listener.proto
│ │ │ │ ├── playeronline/
│ │ │ │ │ └── v1/
│ │ │ │ │ └── playeronline.proto
│ │ │ │ ├── playerunite/
│ │ │ │ │ ├── pgcanymodel/
│ │ │ │ │ │ └── PGCAnyModel.proto
│ │ │ │ │ ├── ugcanymodel/
│ │ │ │ │ │ └── UGCAnyModel.proto
│ │ │ │ │ └── v1/
│ │ │ │ │ └── playerunite.proto
│ │ │ │ ├── playurl/
│ │ │ │ │ └── v1/
│ │ │ │ │ └── playurl.proto
│ │ │ │ ├── resource/
│ │ │ │ │ ├── privacy/
│ │ │ │ │ │ └── v1/
│ │ │ │ │ │ └── api.proto
│ │ │ │ │ └── v1/
│ │ │ │ │ └── module.proto
│ │ │ │ ├── search/
│ │ │ │ │ └── v2/
│ │ │ │ │ └── search.proto
│ │ │ │ ├── show/
│ │ │ │ │ ├── gateway/
│ │ │ │ │ │ └── v1/
│ │ │ │ │ │ └── service.proto
│ │ │ │ │ ├── mixture/
│ │ │ │ │ │ └── v1/
│ │ │ │ │ │ └── mixture.proto
│ │ │ │ │ ├── popular/
│ │ │ │ │ │ └── v1/
│ │ │ │ │ │ └── popular.proto
│ │ │ │ │ ├── rank/
│ │ │ │ │ │ └── v1/
│ │ │ │ │ │ └── rank.proto
│ │ │ │ │ └── region/
│ │ │ │ │ └── v1/
│ │ │ │ │ └── region.proto
│ │ │ │ ├── space/
│ │ │ │ │ └── v1/
│ │ │ │ │ └── space.proto
│ │ │ │ ├── splash/
│ │ │ │ │ └── v1/
│ │ │ │ │ └── splash.proto
│ │ │ │ ├── topic/
│ │ │ │ │ └── v1/
│ │ │ │ │ └── topic.proto
│ │ │ │ ├── view/
│ │ │ │ │ └── v1/
│ │ │ │ │ └── view.proto
│ │ │ │ ├── viewunite/
│ │ │ │ │ ├── common.proto
│ │ │ │ │ ├── pgcanymodel.proto
│ │ │ │ │ ├── ugcanymodel.proto
│ │ │ │ │ └── v1/
│ │ │ │ │ └── viewunite.proto
│ │ │ │ └── wall/
│ │ │ │ └── v1/
│ │ │ │ └── wall.proto
│ │ │ ├── broadcast/
│ │ │ │ ├── message/
│ │ │ │ │ ├── editor/
│ │ │ │ │ │ └── notify.proto
│ │ │ │ │ ├── esports/
│ │ │ │ │ │ └── notify.proto
│ │ │ │ │ ├── fission/
│ │ │ │ │ │ └── notify.proto
│ │ │ │ │ ├── im/
│ │ │ │ │ │ └── notify.proto
│ │ │ │ │ ├── main/
│ │ │ │ │ │ ├── dm.proto
│ │ │ │ │ │ ├── native.proto
│ │ │ │ │ │ ├── resource.proto
│ │ │ │ │ │ └── search.proto
│ │ │ │ │ ├── note/
│ │ │ │ │ │ └── sync.proto
│ │ │ │ │ ├── ogv/
│ │ │ │ │ │ ├── freya.proto
│ │ │ │ │ │ └── live.proto
│ │ │ │ │ ├── ticket/
│ │ │ │ │ │ └── activitygame.proto
│ │ │ │ │ └── tv/
│ │ │ │ │ └── proj.proto
│ │ │ │ ├── v1/
│ │ │ │ │ ├── broadcast.proto
│ │ │ │ │ ├── laser.proto
│ │ │ │ │ ├── mod.proto
│ │ │ │ │ ├── push.proto
│ │ │ │ │ ├── room.proto
│ │ │ │ │ └── test.proto
│ │ │ │ └── v2/
│ │ │ │ └── laser.proto
│ │ │ ├── cheese/
│ │ │ │ └── gateway/
│ │ │ │ └── player/
│ │ │ │ └── v1/
│ │ │ │ └── playurl.proto
│ │ │ ├── community/
│ │ │ │ └── service/
│ │ │ │ ├── dm/
│ │ │ │ │ └── v1/
│ │ │ │ │ └── dm.proto
│ │ │ │ └── govern/
│ │ │ │ └── v1/
│ │ │ │ └── govern.proto
│ │ │ ├── dagw/
│ │ │ │ └── component/
│ │ │ │ └── avatar/
│ │ │ │ ├── common/
│ │ │ │ │ └── common.proto
│ │ │ │ └── v1/
│ │ │ │ ├── avatar.proto
│ │ │ │ └── plugin.proto
│ │ │ ├── dynamic/
│ │ │ │ ├── common/
│ │ │ │ │ └── dynamic.proto
│ │ │ │ ├── gw/
│ │ │ │ │ └── gateway.proto
│ │ │ │ └── interfaces/
│ │ │ │ └── feed/
│ │ │ │ └── v1/
│ │ │ │ └── api.proto
│ │ │ ├── gaia/
│ │ │ │ └── gw/
│ │ │ │ └── gw_api.proto
│ │ │ ├── im/
│ │ │ │ ├── interfaces/
│ │ │ │ │ ├── inner-interface/
│ │ │ │ │ │ └── v1/
│ │ │ │ │ │ └── api.proto
│ │ │ │ │ └── v1/
│ │ │ │ │ └── im.proto
│ │ │ │ └── type/
│ │ │ │ └── im.proto
│ │ │ ├── live/
│ │ │ │ ├── app/
│ │ │ │ │ └── room/
│ │ │ │ │ └── v1/
│ │ │ │ │ └── room.proto
│ │ │ │ └── general/
│ │ │ │ └── interfaces/
│ │ │ │ └── v1/
│ │ │ │ └── interfaces.proto
│ │ │ ├── main/
│ │ │ │ ├── common/
│ │ │ │ │ └── arch/
│ │ │ │ │ └── doll/
│ │ │ │ │ └── v1/
│ │ │ │ │ └── doll.proto
│ │ │ │ └── community/
│ │ │ │ └── reply/
│ │ │ │ └── v1/
│ │ │ │ └── reply.proto
│ │ │ ├── metadata/
│ │ │ │ ├── device/
│ │ │ │ │ └── device.proto
│ │ │ │ ├── fawkes/
│ │ │ │ │ └── fawkes.proto
│ │ │ │ ├── locale/
│ │ │ │ │ └── locale.proto
│ │ │ │ ├── metadata.proto
│ │ │ │ ├── network/
│ │ │ │ │ └── network.proto
│ │ │ │ ├── parabox/
│ │ │ │ │ └── parabox.proto
│ │ │ │ └── restriction/
│ │ │ │ └── restriction.proto
│ │ │ ├── pagination/
│ │ │ │ └── pagination.proto
│ │ │ ├── pangu/
│ │ │ │ └── gallery/
│ │ │ │ └── v1/
│ │ │ │ └── gallery.proto
│ │ │ ├── pgc/
│ │ │ │ ├── gateway/
│ │ │ │ │ └── player/
│ │ │ │ │ ├── v1/
│ │ │ │ │ │ └── playurl.proto
│ │ │ │ │ └── v2/
│ │ │ │ │ └── playurl.proto
│ │ │ │ └── service/
│ │ │ │ └── premiere/
│ │ │ │ └── v1/
│ │ │ │ └── premiere.proto
│ │ │ ├── playershared/
│ │ │ │ └── playershared.proto
│ │ │ ├── polymer/
│ │ │ │ ├── app/
│ │ │ │ │ └── search/
│ │ │ │ │ └── v1/
│ │ │ │ │ └── search.proto
│ │ │ │ ├── community/
│ │ │ │ │ └── govern/
│ │ │ │ │ └── v1/
│ │ │ │ │ └── govern.proto
│ │ │ │ ├── contract/
│ │ │ │ │ └── v1/
│ │ │ │ │ └── contract.proto
│ │ │ │ ├── demo/
│ │ │ │ │ └── demo.proto
│ │ │ │ └── list/
│ │ │ │ └── v1/
│ │ │ │ └── list.proto
│ │ │ ├── relation/
│ │ │ │ └── interfaces/
│ │ │ │ └── api.proto
│ │ │ ├── render/
│ │ │ │ └── render.proto
│ │ │ ├── rpc/
│ │ │ │ └── status.proto
│ │ │ ├── tv/
│ │ │ │ └── interfaces/
│ │ │ │ └── dm/
│ │ │ │ └── v1/
│ │ │ │ └── dm.proto
│ │ │ ├── vega/
│ │ │ │ └── deneb/
│ │ │ │ └── v1/
│ │ │ │ └── deneb.proto
│ │ │ └── web/
│ │ │ ├── interfaces/
│ │ │ │ └── v1/
│ │ │ │ └── interfaces.proto
│ │ │ └── space/
│ │ │ └── v1/
│ │ │ └── space.proto
│ │ ├── common/
│ │ │ └── ErrorProto.proto
│ │ ├── datacenter/
│ │ │ └── hakase/
│ │ │ └── protobuf/
│ │ │ └── android_device_info.proto
│ │ └── pgc/
│ │ ├── biz/
│ │ │ └── room.proto
│ │ └── gateway/
│ │ └── vega/
│ │ └── v1/
│ │ └── vega.proto
│ └── src/
│ ├── main/
│ │ └── kotlin/
│ │ ├── com/
│ │ │ └── tfowl/
│ │ │ └── ktor/
│ │ │ └── client/
│ │ │ └── plugins/
│ │ │ └── JsoupPlugin.kt
│ │ └── dev/
│ │ └── aaa1115910/
│ │ └── biliapi/
│ │ ├── BiliApiConstants.kt
│ │ ├── entity/
│ │ │ ├── ApiType.kt
│ │ │ ├── CarouselData.kt
│ │ │ ├── CodeType.kt
│ │ │ ├── Favorite.kt
│ │ │ ├── Picture.kt
│ │ │ ├── PlayData.kt
│ │ │ ├── danmaku/
│ │ │ │ └── DanmakuMask.kt
│ │ │ ├── home/
│ │ │ │ └── RecommendData.kt
│ │ │ ├── live/
│ │ │ │ ├── LiveArea.kt
│ │ │ │ ├── LiveFollowing.kt
│ │ │ │ ├── LiveRecommend.kt
│ │ │ │ ├── LiveRoom.kt
│ │ │ │ └── LiveRoomPlayInfo.kt
│ │ │ ├── login/
│ │ │ │ ├── Captcha.kt
│ │ │ │ ├── QR.kt
│ │ │ │ └── Sms.kt
│ │ │ ├── pgc/
│ │ │ │ ├── PgcFeedData.kt
│ │ │ │ ├── PgcItem.kt
│ │ │ │ ├── PgcType.kt
│ │ │ │ └── index/
│ │ │ │ ├── IndexParams.kt
│ │ │ │ ├── PgcIndexCondition.kt
│ │ │ │ └── PgcIndexData.kt
│ │ │ ├── rank/
│ │ │ │ └── Popular.kt
│ │ │ ├── reply/
│ │ │ │ ├── Comment.kt
│ │ │ │ ├── CommentPage.kt
│ │ │ │ ├── CommentRepliesData.kt
│ │ │ │ └── CommentSort.kt
│ │ │ ├── search/
│ │ │ │ └── Hotword.kt
│ │ │ ├── season/
│ │ │ │ ├── FollowingSeasons.kt
│ │ │ │ ├── IndexResult.kt
│ │ │ │ └── Timeline.kt
│ │ │ ├── ugc/
│ │ │ │ ├── UgcItem.kt
│ │ │ │ ├── UgcType.kt
│ │ │ │ ├── UgcTypeV2.kt
│ │ │ │ └── region/
│ │ │ │ ├── UgcFeedData.kt
│ │ │ │ ├── UgcFeedPage.kt
│ │ │ │ ├── UgcRegionData.kt
│ │ │ │ ├── UgcRegionListData.kt
│ │ │ │ └── UgcRegionPage.kt
│ │ │ ├── user/
│ │ │ │ ├── Author.kt
│ │ │ │ ├── Dynamic.kt
│ │ │ │ ├── FollowedUser.kt
│ │ │ │ ├── History.kt
│ │ │ │ ├── Space.kt
│ │ │ │ └── ToView.kt
│ │ │ └── video/
│ │ │ ├── Dimension.kt
│ │ │ ├── Heartbeat.kt
│ │ │ ├── RelatedVideo.kt
│ │ │ ├── Subtitle.kt
│ │ │ ├── Tag.kt
│ │ │ ├── VideoDetail.kt
│ │ │ ├── VideoPage.kt
│ │ │ ├── VideoShot.kt
│ │ │ └── season/
│ │ │ ├── Episode.kt
│ │ │ ├── PgcSeason.kt
│ │ │ ├── SeasonDetail.kt
│ │ │ └── Section.kt
│ │ ├── grpc/
│ │ │ └── utils/
│ │ │ ├── Channel.kt
│ │ │ └── StatusExtends.kt
│ │ ├── http/
│ │ │ ├── BiliHttpApi.kt
│ │ │ ├── BiliHttpConstants.kt
│ │ │ ├── BiliHttpProxyApi.kt
│ │ │ ├── BiliLiveHttpApi.kt
│ │ │ ├── BiliPassportHttpApi.kt
│ │ │ ├── BiliPlusHttpApi.kt
│ │ │ ├── entity/
│ │ │ │ ├── BiliResponse.kt
│ │ │ │ ├── biliplus/
│ │ │ │ │ └── View.kt
│ │ │ │ ├── danmaku/
│ │ │ │ │ └── DanmakuResponse.kt
│ │ │ │ ├── dynamic/
│ │ │ │ │ ├── DynamicDetailResponse.kt
│ │ │ │ │ └── DynamicResponse.kt
│ │ │ │ ├── history/
│ │ │ │ │ └── HistoryData.kt
│ │ │ │ ├── home/
│ │ │ │ │ ├── RcmdIndexData.kt
│ │ │ │ │ └── RcmdTopData.kt
│ │ │ │ ├── index/
│ │ │ │ │ ├── IndexFilter.kt
│ │ │ │ │ ├── IndexFilterArea.kt
│ │ │ │ │ ├── IndexFilterProducerId.kt
│ │ │ │ │ ├── IndexFilterStyle.kt
│ │ │ │ │ ├── IndexOrder.kt
│ │ │ │ │ └── IndexResult.kt
│ │ │ │ ├── live/
│ │ │ │ │ ├── HistoryDanmaku.kt
│ │ │ │ │ ├── LiveDanmuInfoResponse.kt
│ │ │ │ │ ├── LiveEvent.kt
│ │ │ │ │ ├── LiveFrame.kt
│ │ │ │ │ └── LiveRoomPlayInfoResponse.kt
│ │ │ │ ├── login/
│ │ │ │ │ ├── Captcha.kt
│ │ │ │ │ ├── qr/
│ │ │ │ │ │ ├── AppQR.kt
│ │ │ │ │ │ └── WebQR.kt
│ │ │ │ │ └── sms/
│ │ │ │ │ ├── SendSmsResponse.kt
│ │ │ │ │ └── SmsLoginResponse.kt
│ │ │ │ ├── pgc/
│ │ │ │ │ ├── PgcFeed.kt
│ │ │ │ │ ├── PgcFeedV3.kt
│ │ │ │ │ └── PgcWebInitialStateData.kt
│ │ │ │ ├── proxy/
│ │ │ │ │ └── PlayUrl.kt
│ │ │ │ ├── region/
│ │ │ │ │ ├── RegionBanner.kt
│ │ │ │ │ ├── RegionDynamic.kt
│ │ │ │ │ ├── RegionDynamicList.kt
│ │ │ │ │ ├── RegionFeedRcmd.kt
│ │ │ │ │ └── RegionLocs.kt
│ │ │ │ ├── reply/
│ │ │ │ │ ├── Comment.kt
│ │ │ │ │ ├── CommentReplyData.kt
│ │ │ │ │ └── Layers.kt
│ │ │ │ ├── search/
│ │ │ │ │ ├── KeywordSuggest.kt
│ │ │ │ │ ├── SearchCost.kt
│ │ │ │ │ ├── SearchResult.kt
│ │ │ │ │ ├── SearchResultItem.kt
│ │ │ │ │ └── SearchSquare.kt
│ │ │ │ ├── season/
│ │ │ │ │ ├── AppSeasonData.kt
│ │ │ │ │ ├── Episode.kt
│ │ │ │ │ ├── Follow.kt
│ │ │ │ │ ├── SeasonSection.kt
│ │ │ │ │ ├── WebFollowingSeason.kt
│ │ │ │ │ └── WebSeasonData.kt
│ │ │ │ ├── subtitle/
│ │ │ │ │ └── Subtitle.kt
│ │ │ │ ├── toview/
│ │ │ │ │ └── ToViewData.kt
│ │ │ │ ├── user/
│ │ │ │ │ ├── Follow.kt
│ │ │ │ │ ├── LevelInfo.kt
│ │ │ │ │ ├── Nameplate.kt
│ │ │ │ │ ├── Official.kt
│ │ │ │ │ ├── Pendant.kt
│ │ │ │ │ ├── Profession.kt
│ │ │ │ │ ├── Relation.kt
│ │ │ │ │ ├── SpaceVideoData.kt
│ │ │ │ │ ├── Staff.kt
│ │ │ │ │ ├── UserCardInfoResponse.kt
│ │ │ │ │ ├── UserGarb.kt
│ │ │ │ │ ├── UserHonours.kt
│ │ │ │ │ ├── UserInfoResponse.kt
│ │ │ │ │ ├── UserSelfInfoResponse.kt
│ │ │ │ │ ├── Vip.kt
│ │ │ │ │ ├── favorite/
│ │ │ │ │ │ ├── CntInfo.kt
│ │ │ │ │ │ ├── FavoriteFolderInfo.kt
│ │ │ │ │ │ ├── FavoriteFolderInfoListData.kt
│ │ │ │ │ │ ├── FavoriteItem.kt
│ │ │ │ │ │ ├── Upper.kt
│ │ │ │ │ │ └── UserFavoriteFoldersData.kt
│ │ │ │ │ └── garb/
│ │ │ │ │ ├── CardBg.kt
│ │ │ │ │ ├── Equip.kt
│ │ │ │ │ └── Item.kt
│ │ │ │ ├── video/
│ │ │ │ │ ├── AddCoin.kt
│ │ │ │ │ ├── ArchiveRelation.kt
│ │ │ │ │ ├── GaiaVgateData.kt
│ │ │ │ │ ├── PlayUrlResponse.kt
│ │ │ │ │ ├── PopularVideosResponse.kt
│ │ │ │ │ ├── RelatedVideosResponse.kt
│ │ │ │ │ ├── SetVideoFavorite.kt
│ │ │ │ │ ├── Tag.kt
│ │ │ │ │ ├── Timeline.kt
│ │ │ │ │ ├── UgcSeason.kt
│ │ │ │ │ ├── VideoDetail.kt
│ │ │ │ │ ├── VideoInfo.kt
│ │ │ │ │ ├── VideoMoreInfo.kt
│ │ │ │ │ ├── VideoOnlineTotal.kt
│ │ │ │ │ └── VideoShot.kt
│ │ │ │ └── web/
│ │ │ │ ├── Hover.kt
│ │ │ │ └── Nav.kt
│ │ │ ├── plugins/
│ │ │ │ └── BiliUserAgent.kt
│ │ │ └── util/
│ │ │ ├── ApiSign.kt
│ │ │ ├── BiliAppConf.kt
│ │ │ ├── BiliDns.kt
│ │ │ ├── BiliWebConf.kt
│ │ │ ├── Brotli.kt
│ │ │ ├── Buvid.kt
│ │ │ ├── CommonEnumIntSerializer.kt
│ │ │ └── Zlib.kt
│ │ ├── repositories/
│ │ │ ├── AuthRepository.kt
│ │ │ ├── BiliApiModule.kt
│ │ │ ├── ChannelRepository.kt
│ │ │ ├── CoinRepository.kt
│ │ │ ├── CommentRepository.kt
│ │ │ ├── FavoriteRepository.kt
│ │ │ ├── HistoryRepository.kt
│ │ │ ├── LikeRepository.kt
│ │ │ ├── LiveRepository.kt
│ │ │ ├── LoginRepository.kt
│ │ │ ├── PgcRepository.kt
│ │ │ ├── RecommendVideoRepository.kt
│ │ │ ├── SearchRepository.kt
│ │ │ ├── SeasonRepository.kt
│ │ │ ├── ToViewRepository.kt
│ │ │ ├── UgcRepository.kt
│ │ │ ├── UserRepository.kt
│ │ │ ├── VideoDetailRepository.kt
│ │ │ └── VideoPlayRepository.kt
│ │ ├── util/
│ │ │ ├── AvBvConverter.kt
│ │ │ ├── Extends.kt
│ │ │ └── UrlUtil.kt
│ │ └── websocket/
│ │ └── LiveDataWebSocket.kt
│ └── test/
│ ├── kotlin/
│ │ └── dev/
│ │ └── aaa1115910/
│ │ └── biliapi/
│ │ ├── BvLoginRepositoryTest.kt
│ │ ├── entity/
│ │ │ └── DanmakuMaskTest.kt
│ │ ├── http/
│ │ │ ├── BiliHttpApiTest.kt
│ │ │ ├── BiliLiveHttpApiTest.kt
│ │ │ ├── BiliPassportHttpApiTest.kt
│ │ │ └── BiliPlusHttpApiTest.kt
│ │ ├── repositories/
│ │ │ ├── CommentRepositoryTest.kt
│ │ │ ├── FavoriteRepositoryTest.kt
│ │ │ ├── HistoryRepositoryTest.kt
│ │ │ ├── PgcRepositoryTest.kt
│ │ │ ├── RecommendVideoRepositoryTest.kt
│ │ │ ├── SearchRepositoryTest.kt
│ │ │ ├── SeasonRepositoryTest.kt
│ │ │ ├── UgcRepositoryTest.kt
│ │ │ ├── UserRepositoryTest.kt
│ │ │ ├── VideoDetailRepositoryTest.kt
│ │ │ └── VideoPlayRepositoryTest.kt
│ │ └── websocket/
│ │ └── LiveDataWebSocketTest.kt
│ └── resources/
│ ├── 3540266_25_2.exp.mobmask
│ └── 3540266_25_2.exp.webmask
├── bili-subtitle/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ └── kotlin/
│ │ └── dev/
│ │ └── aaa1115910/
│ │ └── bilisubtitle/
│ │ ├── SubtitleEncoder.kt
│ │ ├── SubtitleParser.kt
│ │ └── entity/
│ │ ├── BiliSubtitle.kt
│ │ ├── SrtSubtitle.kt
│ │ ├── SubtitleItem.kt
│ │ └── Timestamp.kt
│ └── test/
│ ├── kotlin/
│ │ └── dev/
│ │ └── aaa1115910/
│ │ └── bilisubtitle/
│ │ ├── SubtitleEncoderTest.kt
│ │ ├── SubtitleParserTest.kt
│ │ └── entity/
│ │ └── TimestampTest.kt
│ └── resources/
│ ├── example.bcc
│ └── example.srt
├── build.gradle.kts
├── doc/
│ └── 弹幕/
│ ├── calc_danmaku_averages.js
│ ├── 弹幕code review 报告.md
│ ├── 弹幕库优化.md
│ ├── 弹幕重构需求.md
│ └── 重构后.txt
├── gradle/
│ ├── androidx.versions.toml
│ ├── gradle.versions.toml
│ ├── libs.versions.toml
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── player/
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── core/
│ │ ├── .gitignore
│ │ ├── build.gradle.kts
│ │ ├── consumer-rules.pro
│ │ ├── proguard-rules.pro
│ │ └── src/
│ │ └── main/
│ │ ├── AndroidManifest.xml
│ │ ├── assets/
│ │ │ └── GlobalSign ECC Root CA R5.crt
│ │ └── kotlin/
│ │ └── dev/
│ │ └── aaa1115910/
│ │ └── bv/
│ │ └── player/
│ │ ├── AbstractVideoPlayer.kt
│ │ ├── BvVideoPlayer.kt
│ │ ├── OkHttpUtil.kt
│ │ ├── VideoPlayerListener.kt
│ │ ├── VideoPlayerOptions.kt
│ │ ├── factory/
│ │ │ └── PlayerFactory.kt
│ │ └── impl/
│ │ └── exo/
│ │ ├── ExoMediaPlayer.kt
│ │ └── ExoPlayerFactory.kt
│ ├── mobile/
│ │ ├── .gitignore
│ │ ├── build.gradle.kts
│ │ ├── consumer-rules.pro
│ │ ├── proguard-rules.pro
│ │ └── src/
│ │ └── main/
│ │ ├── AndroidManifest.xml
│ │ └── kotlin/
│ │ └── dev/
│ │ └── aaa1115910/
│ │ └── bv/
│ │ └── player/
│ │ └── mobile/
│ │ ├── BvPlayer.kt
│ │ ├── MaterialDarkTheme.kt
│ │ ├── Media3VideoPlayer.kt
│ │ ├── NoRippleClickable.kt
│ │ ├── SeekBar.kt
│ │ └── controller/
│ │ ├── BvPlayerController.kt
│ │ ├── FullscreenControllers.kt
│ │ ├── MiniControllers.kt
│ │ ├── Tips.kt
│ │ └── menu/
│ │ ├── DanmakuMenu.kt
│ │ ├── DashMenu.kt
│ │ ├── MoreMenu.kt
│ │ ├── SpeedMenu.kt
│ │ └── VideoListMenu.kt
│ ├── shared/
│ │ ├── .gitignore
│ │ ├── build.gradle.kts
│ │ ├── consumer-rules.pro
│ │ ├── proguard-rules.pro
│ │ └── src/
│ │ └── main/
│ │ ├── AndroidManifest.xml
│ │ ├── kotlin/
│ │ │ └── dev/
│ │ │ └── aaa1115910/
│ │ │ └── bv/
│ │ │ └── player/
│ │ │ ├── danmaku/
│ │ │ │ ├── CacheManager.kt
│ │ │ │ ├── DanmakuConfig.kt
│ │ │ │ ├── DanmakuEngine.kt
│ │ │ │ ├── DanmakuLogStats.kt
│ │ │ │ ├── DanmakuPlayer.kt
│ │ │ │ ├── DanmakuTimer.kt
│ │ │ │ ├── DanmakuView.kt
│ │ │ │ └── model/
│ │ │ │ ├── Danmaku.kt
│ │ │ │ ├── DanmakuItem.kt
│ │ │ │ ├── DanmakuKind.kt
│ │ │ │ └── RenderSnapshot.kt
│ │ │ ├── entity/
│ │ │ │ ├── Audio.kt
│ │ │ │ ├── ControllerButtonConfig.kt
│ │ │ │ ├── DanmakuSize.kt
│ │ │ │ ├── DanmakuTransparency.kt
│ │ │ │ ├── DanmakuType.kt
│ │ │ │ ├── DefaultStartPosition.kt
│ │ │ │ ├── LiveCodec.kt
│ │ │ │ ├── PlayMode.kt
│ │ │ │ ├── PortraitVideoFixMode.kt
│ │ │ │ ├── RequestState.kt
│ │ │ │ ├── Resolution.kt
│ │ │ │ ├── VideoAspectRatio.kt
│ │ │ │ ├── VideoCodec.kt
│ │ │ │ ├── VideoListItem.kt
│ │ │ │ ├── VideoPlayerClosedCaptionMenuItem.kt
│ │ │ │ ├── VideoPlayerDanmakuMenuItem.kt
│ │ │ │ ├── VideoPlayerData.kt
│ │ │ │ ├── VideoPlayerMenuNavItem.kt
│ │ │ │ ├── VideoPlayerOthersMenuItem.kt
│ │ │ │ ├── VideoPlayerPictureMenuItem.kt
│ │ │ │ └── VideoRotation.kt
│ │ │ ├── seekbar/
│ │ │ │ ├── SeekBar.kt
│ │ │ │ ├── SeekBarThumb.kt
│ │ │ │ └── SeekMoveState.kt
│ │ │ └── util/
│ │ │ ├── DanmakuMaskFinder.kt
│ │ │ ├── DanmakuMaskModifiers.kt
│ │ │ └── VideoShotExtends.kt
│ │ └── res/
│ │ ├── drawable/
│ │ │ ├── ic_danmaku_hide.xml
│ │ │ ├── ic_danmaku_on.xml
│ │ │ ├── ic_play_mode_custom.xml
│ │ │ ├── ic_play_mode_list_order.xml
│ │ │ ├── ic_play_mode_list_order_reverse.xml
│ │ │ ├── ic_play_mode_part_and_episode.xml
│ │ │ ├── ic_play_mode_part_and_episode_reverse.xml
│ │ │ ├── ic_play_mode_related_video.xml
│ │ │ ├── ic_play_mode_single.xml
│ │ │ ├── ic_play_mode_single_loop.xml
│ │ │ ├── ic_subtitle_off.xml
│ │ │ ├── ic_subtitle_on.xml
│ │ │ ├── next_play_fill.xml
│ │ │ ├── person.xml
│ │ │ └── person_following.xml
│ │ └── values/
│ │ └── strings.xml
│ └── tv/
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── consumer-rules.pro
│ ├── proguard-rules.pro
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ └── kotlin/
│ └── dev/
│ └── aaa1115910/
│ └── bv/
│ └── player/
│ └── tv/
│ ├── BvPlayer.kt
│ ├── SeekBar.kt
│ └── controller/
│ ├── BottomSubtitle.kt
│ ├── ControllerVideoInfo.kt
│ ├── LiveViewerCountTip.kt
│ ├── MenuController.kt
│ ├── OnlineViewerCountTip.kt
│ ├── PlayStateTips.kt
│ ├── SeekController.kt
│ ├── SkipTip.kt
│ ├── UserActionContent.kt
│ ├── VideoListController.kt
│ ├── VideoPlayerController.kt
│ ├── VideoShot.kt
│ └── playermenu/
│ ├── ClosedCaptionMenu.kt
│ ├── DanmakuMenu.kt
│ ├── MenuNav.kt
│ ├── OthersMenu.kt
│ ├── PictureMenu.kt
│ └── component/
│ ├── CheckBoxMenuList.kt
│ ├── MenuListItem.kt
│ ├── RadioMenuList.kt
│ └── StepLessMenuItem.kt
├── settings.gradle.kts
├── symbols/
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src/
│ └── main/
│ └── kotlin/
│ └── dev/
│ └── aaa1115910/
│ └── symbols/
│ └── Symbols.kt
└── utils/
├── .gitignore
├── build.gradle.kts
├── consumer-rules.pro
├── proguard-rules.pro
└── src/
└── main/
├── AndroidManifest.xml
└── kotlin/
└── dev/
└── aaa1115910/
└── bv/
└── util/
├── DateExtends.kt
├── Debounce.kt
├── FirebaseUtil.kt
├── FocusRequesterExtends.kt
├── ImageExtends.kt
├── KLoggerExtends.kt
├── KeyEventExtends.kt
├── LongExtends.kt
├── SnapshotStateListExtends.kt
├── Timer.kt
├── ToastExtends.kt
├── createCustomInitialFocusRestorerModifiers.kt
└── ifElse.kt
SYMBOL INDEX (7 symbols across 1 files)
FILE: doc/弹幕/calc_danmaku_averages.js
function parseMaxLogs (line 111) | function parseMaxLogs(value) {
function createBucket (line 130) | function createBucket() {
function createMemoryBucket (line 151) | function createMemoryBucket() {
function average (line 160) | function average(total, count) {
function printStats (line 168) | function printStats(type, bucket) {
function printCacheQStats (line 198) | function printCacheQStats(bucket) {
function printMemoryStats (line 212) | function printMemoryStats(bucket) {
Condensed preview — 945 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (5,666K chars).
[
{
"path": ".github/ISSUE_TEMPLATE/bug_report.yml",
"chars": 2391,
"preview": "name: Bug 报告\ndescription: 创建 Bug 报告以帮助开发者改进\ntitle: \"以简单的一段字概括你所遇到的问题\"\nbody:\n - type: markdown\n attributes:\n val"
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 27,
"preview": "blank_issues_enabled: false"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.yml",
"chars": 960,
"preview": "name: 功能需求\ndescription: 为项目提供建议\ntitle: \"以简单的一段字概括你的建议\"\nbody:\n - type: markdown\n attributes:\n value: |\n #"
},
{
"path": ".github/workflows/alpha.yml",
"chars": 4582,
"preview": "name: Alpha Build\n\non:\n push:\n branches:\n - develop\n\njobs:\n build-alpha:\n name: Build Alpha Apk\n runs-on"
},
{
"path": ".github/workflows/alpha_build_manually_without_sign.yml",
"chars": 2862,
"preview": "name: Alpha Build Manually (Without signature)\n\non:\n workflow_dispatch:\n inputs:\n google_services_json:\n "
},
{
"path": ".github/workflows/auto_close_issues.yml",
"chars": 949,
"preview": "name: Check Issues\n\non:\n issues:\n types: [ opened ]\njobs:\n check:\n runs-on: ubuntu-latest\n steps:\n - if:"
},
{
"path": ".github/workflows/close_inactive_issues.yml",
"chars": 626,
"preview": "name: Close inactive issues\non:\n schedule:\n - cron: \"30 1 * * *\"\n\njobs:\n close-issues:\n name: Close inactive iss"
},
{
"path": ".github/workflows/features.yml",
"chars": 3162,
"preview": "name: Feature Build\n\non:\n push:\n branches:\n - 'feature/**'\n\njobs:\n build-alpha:\n name: Build Feature Apk\n "
},
{
"path": ".github/workflows/release.yml",
"chars": 4375,
"preview": "name: Release Build\n\non:\n push:\n tags:\n - 'v[0-9]+.[0-9]+.[0-9]+'\n - 'v[0-9]+.[0-9]+.[0-9]+.[0-9]+'\n\njobs:"
},
{
"path": ".gitignore",
"chars": 258,
"preview": "*.iml\n.idea\n.gradle\n/local.properties\n.DS_Store\nbuild\n/captures\n.externalNativeBuild\n.cxx\n/signing.properties\n/.idea/jar"
},
{
"path": ".gitmodules",
"chars": 81,
"preview": "[submodule \"libs\"]\n\tpath = libs\n\turl = https://github.com/aaa1115910/bv-libs.git\n"
},
{
"path": "CHANGELOG.md",
"chars": 3176,
"preview": "[](https://github.com/fantasy"
},
{
"path": "LICENSE",
"chars": 1066,
"preview": "MIT License\n\nCopyright (c) 2022 aaa1115910\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
},
{
"path": "README.md",
"chars": 4779,
"preview": "<div align=\"center\">\n\n<img src=\"app/shared/src/main/res/drawable/ic_banner_md.webp\" style=\"border-radius: 24px; margin-t"
},
{
"path": "app/.gitignore",
"chars": 52,
"preview": "/build\n/google-services.json\n/release\n/r8Test\n/debug"
},
{
"path": "app/build.gradle.kts",
"chars": 5167,
"preview": "@file:Suppress(\"UnstableApiUsage\")\n\nimport com.android.build.gradle.internal.api.ApkVariantOutputImpl\nimport java.io.Fil"
},
{
"path": "app/compose_compiler_config.conf",
"chars": 361,
"preview": "kotlin.collections.*\nkotlin.time.Duration\n\nkotlinx.coroutines.CoroutineScope\n\nandroidx.paging.compose.LazyPagingItems\n\n#"
},
{
"path": "app/mobile/.gitignore",
"chars": 6,
"preview": "/build"
},
{
"path": "app/mobile/build.gradle.kts",
"chars": 1531,
"preview": "plugins {\n alias(gradleLibs.plugins.android.library)\n alias(gradleLibs.plugins.compose.compiler)\n alias(gradleL"
},
{
"path": "app/mobile/consumer-rules.pro",
"chars": 0,
"preview": ""
},
{
"path": "app/mobile/proguard-rules.pro",
"chars": 750,
"preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
},
{
"path": "app/mobile/src/main/AndroidManifest.xml",
"chars": 3496,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n <appli"
},
{
"path": "app/mobile/src/main/kotlin/com/origeek/imageViewer/gallery/ImageGallery.kt",
"chars": 5722,
"preview": "package com.origeek.imageViewer.gallery\n\nimport androidx.annotation.FloatRange\nimport androidx.annotation.IntRange\nimpor"
},
{
"path": "app/mobile/src/main/kotlin/com/origeek/imageViewer/gallery/ImagePager.kt",
"chars": 2755,
"preview": "package com.origeek.imageViewer.gallery\n\nimport androidx.annotation.FloatRange\nimport androidx.annotation.IntRange\nimpor"
},
{
"path": "app/mobile/src/main/kotlin/com/origeek/imageViewer/previewer/ImagePreviewer.kt",
"chars": 10163,
"preview": "package com.origeek.imageViewer.previewer\n\nimport androidx.annotation.IntRange\nimport androidx.compose.animation.Animate"
},
{
"path": "app/mobile/src/main/kotlin/com/origeek/imageViewer/previewer/ImageTransform.kt",
"chars": 17918,
"preview": "package com.origeek.imageViewer.previewer\n\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.ani"
},
{
"path": "app/mobile/src/main/kotlin/com/origeek/imageViewer/previewer/ImageViewerContainer.kt",
"chars": 9727,
"preview": "package com.origeek.imageViewer.previewer\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose."
},
{
"path": "app/mobile/src/main/kotlin/com/origeek/imageViewer/previewer/PreviewerPagerState.kt",
"chars": 1118,
"preview": "package com.origeek.imageViewer.previewer\n\nimport androidx.annotation.FloatRange\nimport androidx.annotation.IntRange\nimp"
},
{
"path": "app/mobile/src/main/kotlin/com/origeek/imageViewer/previewer/PreviewerTransformState.kt",
"chars": 12259,
"preview": "package com.origeek.imageViewer.previewer\n\nimport androidx.compose.animation.EnterTransition\nimport androidx.compose.ani"
},
{
"path": "app/mobile/src/main/kotlin/com/origeek/imageViewer/previewer/PreviewerVerticalDragState.kt",
"chars": 8215,
"preview": "package com.origeek.imageViewer.previewer\n\nimport androidx.compose.animation.core.AnimationSpec\nimport androidx.compose."
},
{
"path": "app/mobile/src/main/kotlin/com/origeek/imageViewer/util/Ticket.kt",
"chars": 1045,
"preview": "package com.origeek.imageViewer.util\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Launche"
},
{
"path": "app/mobile/src/main/kotlin/com/origeek/imageViewer/viewer/ImageComposeCanvas.kt",
"chars": 25111,
"preview": "package com.origeek.imageViewer.viewer\n\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport andr"
},
{
"path": "app/mobile/src/main/kotlin/com/origeek/imageViewer/viewer/ImageComposeOrigin.kt",
"chars": 8817,
"preview": "package com.origeek.imageViewer.viewer\n\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animat"
},
{
"path": "app/mobile/src/main/kotlin/com/origeek/imageViewer/viewer/ImageViewer.kt",
"chars": 28652,
"preview": "package com.origeek.imageViewer.viewer\n\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animat"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/activities/DynamicDetailActivity.kt",
"chars": 1068,
"preview": "package dev.aaa1115910.bv.mobile.activities\n\nimport android.content.Context\nimport android.content.Intent\nimport android"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/activities/FavoriteActivity.kt",
"chars": 869,
"preview": "package dev.aaa1115910.bv.mobile.activities\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport "
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/activities/FollowingSeasonActivity.kt",
"chars": 890,
"preview": "package dev.aaa1115910.bv.mobile.activities\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport "
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/activities/FollowingUserActivity.kt",
"chars": 539,
"preview": "package dev.aaa1115910.bv.mobile.activities\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport "
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/activities/HistoryActivity.kt",
"chars": 866,
"preview": "package dev.aaa1115910.bv.mobile.activities\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport "
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/activities/IntentHandlerActivity.kt",
"chars": 706,
"preview": "package dev.aaa1115910.bv.mobile.activities\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport "
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/activities/LoginActivity.kt",
"chars": 515,
"preview": "package dev.aaa1115910.bv.mobile.activities\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport "
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/activities/MainActivity.kt",
"chars": 1870,
"preview": "package dev.aaa1115910.bv.mobile.activities\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport "
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/activities/QrTokenResultActivity.kt",
"chars": 973,
"preview": "package dev.aaa1115910.bv.mobile.activities\n\nimport android.content.Context\nimport android.content.Intent\nimport android"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/activities/SettingsActivity.kt",
"chars": 534,
"preview": "package dev.aaa1115910.bv.mobile.activities\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport "
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/activities/UserSpaceActivity.kt",
"chars": 919,
"preview": "package dev.aaa1115910.bv.mobile.activities\n\nimport android.content.Context\nimport android.content.Intent\nimport android"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/activities/VideoPlayerActivity.kt",
"chars": 5680,
"preview": "package dev.aaa1115910.bv.mobile.activities\n\nimport android.content.Context\nimport android.content.Intent\nimport android"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/home/SearchBar.kt",
"chars": 12692,
"preview": "package dev.aaa1115910.bv.mobile.component.home\n\nimport android.content.Context\nimport androidx.compose.animation.core.a"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/home/UserDialog.kt",
"chars": 21517,
"preview": "package dev.aaa1115910.bv.mobile.component.home\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.co"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/home/dynamic/DynamicItem.kt",
"chars": 32768,
"preview": "package dev.aaa1115910.bv.mobile.component.home.dynamic\n\nimport android.content.res.Configuration\nimport androidx.compos"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/home/dynamic/DynamicUserItem.kt",
"chars": 1621,
"preview": "package dev.aaa1115910.bv.mobile.component.home.dynamic\n\nimport android.content.res.Configuration\nimport androidx.compos"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/player/VideoPlayerPages.kt",
"chars": 27617,
"preview": "package dev.aaa1115910.bv.mobile.component.player\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx."
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/preferences/PreferenceGroup.kt",
"chars": 3618,
"preview": "package dev.aaa1115910.bv.mobile.component.preferences\n\nimport androidx.compose.foundation.layout.Spacer\nimport androidx"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/preferences/PreferencesPreview.kt",
"chars": 4459,
"preview": "package dev.aaa1115910.bv.mobile.component.preferences\n\nimport android.content.res.Configuration\nimport androidx.compose"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/preferences/items/BaseListItem.kt",
"chars": 2555,
"preview": "package dev.aaa1115910.bv.mobile.component.preferences.items\n\nimport androidx.compose.foundation.clickable\nimport androi"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/preferences/items/ListItemPreference.kt",
"chars": 4310,
"preview": "package dev.aaa1115910.bv.mobile.component.preferences.items\n\nimport android.content.res.Configuration\nimport androidx.c"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/preferences/items/RadioPreference.kt",
"chars": 7272,
"preview": "package dev.aaa1115910.bv.mobile.component.preferences.items\n\nimport android.content.res.Configuration\nimport androidx.c"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/preferences/items/SwitchPreference.kt",
"chars": 4614,
"preview": "package dev.aaa1115910.bv.mobile.component.preferences.items\n\nimport android.content.res.Configuration\nimport androidx.c"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/preferences/items/TextPreference.kt",
"chars": 3582,
"preview": "package dev.aaa1115910.bv.mobile.component.preferences.items\n\nimport android.content.res.Configuration\nimport androidx.c"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/reply/CommentItem.kt",
"chars": 22935,
"preview": "package dev.aaa1115910.bv.mobile.component.reply\n\nimport androidx.compose.animation.core.animateIntAsState\nimport androi"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/reply/Comments.kt",
"chars": 6150,
"preview": "package dev.aaa1115910.bv.mobile.component.reply\n\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport an"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/reply/Replies.kt",
"chars": 6052,
"preview": "package dev.aaa1115910.bv.mobile.component.reply\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx."
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/reply/ReplySheetScaffold.kt",
"chars": 8611,
"preview": "package dev.aaa1115910.bv.mobile.component.reply\n\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.f"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/search/PgcCard.kt",
"chars": 51,
"preview": "package dev.aaa1115910.bv.mobile.component.search\n\n"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/search/UgcCard.kt",
"chars": 6354,
"preview": "package dev.aaa1115910.bv.mobile.component.search\n\nimport android.content.res.Configuration\nimport androidx.compose.foun"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/search/UserCard.kt",
"chars": 51,
"preview": "package dev.aaa1115910.bv.mobile.component.search\n\n"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/settings/UpdateDialog.kt",
"chars": 1023,
"preview": "package dev.aaa1115910.bv.mobile.component.settings\n\nimport androidx.compose.material3.Button\nimport androidx.compose.ma"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/user/UserAvatar.kt",
"chars": 1482,
"preview": "package dev.aaa1115910.bv.mobile.component.user\n\nimport android.content.res.Configuration\nimport androidx.compose.founda"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/videocard/RelatedVideoItem.kt",
"chars": 6800,
"preview": "package dev.aaa1115910.bv.mobile.component.videocard\n\nimport androidx.compose.foundation.background\nimport androidx.comp"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/videocard/SeasonCard.kt",
"chars": 5980,
"preview": "package dev.aaa1115910.bv.mobile.component.videocard\n\nimport androidx.compose.foundation.background\nimport androidx.comp"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/videocard/SmallVideoCard.kt",
"chars": 7816,
"preview": "package dev.aaa1115910.bv.mobile.component.videocard\n\nimport androidx.compose.foundation.background\nimport androidx.comp"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/videocard/UpIcon.kt",
"chars": 974,
"preview": "package dev.aaa1115910.bv.mobile.component.videocard\n\nimport androidx.compose.foundation.layout.Row\nimport androidx.comp"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/videocard/UpSpaceVideoItem.kt",
"chars": 6187,
"preview": "package dev.aaa1115910.bv.mobile.component.videocard\n\nimport androidx.compose.foundation.background\nimport androidx.comp"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/DynamicDetailScreen.kt",
"chars": 24934,
"preview": "package dev.aaa1115910.bv.mobile.screen\n\nimport android.app.Activity\nimport android.content.Context\nimport androidx.acti"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/FavoriteScreen.kt",
"chars": 9342,
"preview": "package dev.aaa1115910.bv.mobile.screen\n\nimport android.app.Activity\nimport androidx.compose.foundation.layout.Arrangeme"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/FollowingSeasonScreen.kt",
"chars": 6275,
"preview": "package dev.aaa1115910.bv.mobile.screen\n\nimport android.app.Activity\nimport androidx.compose.foundation.layout.Arrangeme"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/FollowingUserScreen.kt",
"chars": 7571,
"preview": "package dev.aaa1115910.bv.mobile.screen\n\nimport android.app.Activity\nimport androidx.compose.foundation.background\nimpor"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/HistoryScreen.kt",
"chars": 3987,
"preview": "package dev.aaa1115910.bv.mobile.screen\n\nimport android.app.Activity\nimport androidx.compose.foundation.layout.Arrangeme"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/LoginScreen.kt",
"chars": 17622,
"preview": "package dev.aaa1115910.bv.mobile.screen\n\nimport android.app.Activity\nimport androidx.compose.animation.AnimatedVisibilit"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/MobileMainScreen.kt",
"chars": 23314,
"preview": "package dev.aaa1115910.bv.mobile.screen\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport andro"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/QrTokenResultScreen.kt",
"chars": 10316,
"preview": "package dev.aaa1115910.bv.mobile.screen\n\nimport android.app.Activity\nimport android.content.Intent\nimport android.conten"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/RegionBlockScreen.kt",
"chars": 3557,
"preview": "package dev.aaa1115910.bv.mobile.screen\n\nimport android.app.Activity\nimport android.content.res.Configuration\nimport and"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/UserSpaceScreen.kt",
"chars": 3356,
"preview": "package dev.aaa1115910.bv.mobile.screen\n\nimport android.app.Activity\nimport androidx.compose.foundation.layout.Spacer\nim"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/VideoPlayerScreen.kt",
"chars": 49843,
"preview": "package dev.aaa1115910.bv.mobile.screen\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport andro"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/home/DynamicScreen.kt",
"chars": 6971,
"preview": "package dev.aaa1115910.bv.mobile.screen.home\n\nimport android.app.Activity\nimport androidx.compose.foundation.background\n"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/home/HomeScreen.kt",
"chars": 10218,
"preview": "package dev.aaa1115910.bv.mobile.screen.home\n\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androi"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/home/SearchScreen.kt",
"chars": 16843,
"preview": "package dev.aaa1115910.bv.mobile.screen.home\n\nimport android.app.Activity\nimport android.content.res.Configuration\nimpor"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/home/home/PopularPage.kt",
"chars": 2971,
"preview": "package dev.aaa1115910.bv.mobile.screen.home.home\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/home/home/RcmdPage.kt",
"chars": 2965,
"preview": "package dev.aaa1115910.bv.mobile.screen.home.home\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/home/search/SearchInput.kt",
"chars": 5962,
"preview": "package dev.aaa1115910.bv.mobile.screen.home.search\n\nimport androidx.compose.animation.AnimatedVisibilityScope\nimport an"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/home/search/SearchResult.kt",
"chars": 12387,
"preview": "package dev.aaa1115910.bv.mobile.screen.home.search\n\nimport android.app.Activity\nimport androidx.compose.animation.Anima"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/settings/SettingsCategories.kt",
"chars": 3948,
"preview": "package dev.aaa1115910.bv.mobile.screen.settings\n\nimport androidx.compose.foundation.layout.PaddingValues\nimport android"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/settings/SettingsDetails.kt",
"chars": 2585,
"preview": "package dev.aaa1115910.bv.mobile.screen.settings\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose."
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/settings/SettingsScreen.kt",
"chars": 5400,
"preview": "package dev.aaa1115910.bv.mobile.screen.settings\n\nimport android.app.Activity\nimport androidx.activity.compose.BackHandl"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/settings/details/AboutContent.kt",
"chars": 4102,
"preview": "package dev.aaa1115910.bv.mobile.screen.settings.details\n\nimport android.content.Intent\nimport android.content.res.Confi"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/settings/details/AdvanceContent.kt",
"chars": 2672,
"preview": "package dev.aaa1115910.bv.mobile.screen.settings.details\n\nimport android.content.res.Configuration\nimport androidx.compo"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/settings/details/DebugContent.kt",
"chars": 860,
"preview": "package dev.aaa1115910.bv.mobile.screen.settings.details\n\nimport android.content.res.Configuration\nimport androidx.compo"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/settings/details/PlayContent.kt",
"chars": 2562,
"preview": "package dev.aaa1115910.bv.mobile.screen.settings.details\n\nimport android.content.res.Configuration\nimport androidx.compo"
},
{
"path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/theme/Theme.kt",
"chars": 2305,
"preview": "package dev.aaa1115910.bv.mobile.theme\n\nimport android.app.Activity\nimport android.os.Build\nimport androidx.compose.foun"
},
{
"path": "app/mobile/src/main/res/values/strings.xml",
"chars": 1004,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"no\"?>\n<resources>\n <string name=\"qr_login_button_login\">二维码登录</strin"
},
{
"path": "app/mobile/src/main/res/values/themes.xml",
"chars": 357,
"preview": "<resources>\n\n <style name=\"Theme.BV.Mobile.Splash\" parent=\"Theme.SplashScreen\">\n <item name=\"postSplashScreenT"
},
{
"path": "app/proguard-rules.pro",
"chars": 4885,
"preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
},
{
"path": "app/shared/.gitignore",
"chars": 38,
"preview": "/build\n/src/main/res/raw/blacklist.bin"
},
{
"path": "app/shared/build.gradle.kts",
"chars": 5938,
"preview": "import java.net.URI\n\nplugins {\n alias(gradleLibs.plugins.android.library)\n alias(gradleLibs.plugins.compose.compil"
},
{
"path": "app/shared/consumer-rules.pro",
"chars": 0,
"preview": ""
},
{
"path": "app/shared/proguard-rules.pro",
"chars": 750,
"preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
},
{
"path": "app/shared/schemas/dev.aaa1115910.bv.dao.AppDatabase/1.json",
"chars": 1302,
"preview": "{\n \"formatVersion\": 1,\n \"database\": {\n \"version\": 1,\n \"identityHash\": \"cfa90a83d1b95f8f5ed276a5dd3120c7\",\n \"e"
},
{
"path": "app/shared/schemas/dev.aaa1115910.bv.dao.AppDatabase/2.json",
"chars": 2527,
"preview": "{\n \"formatVersion\": 1,\n \"database\": {\n \"version\": 2,\n \"identityHash\": \"c33e0c010c4133482ddbbdce8d56428c\",\n \"e"
},
{
"path": "app/shared/schemas/dev.aaa1115910.bv.dao.AppDatabase/3.json",
"chars": 2746,
"preview": "{\n \"formatVersion\": 1,\n \"database\": {\n \"version\": 3,\n \"identityHash\": \"ad0905227bbe6c87b6048b4124cf310d\",\n \"e"
},
{
"path": "app/shared/src/debug/AndroidManifest.xml",
"chars": 158,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n <appli"
},
{
"path": "app/shared/src/debug/res/values/strings.xml",
"chars": 125,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<resources>\n <string name=\"app_name\">BV Debug</string>\n</resou"
},
{
"path": "app/shared/src/main/AndroidManifest.xml",
"chars": 1489,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:to"
},
{
"path": "app/shared/src/main/kotlin/coil/transform/BlurTransformation.kt",
"chars": 2801,
"preview": "@file:Suppress(\"DEPRECATION\")\n\npackage coil.transform\n\nimport android.content.Context\nimport android.graphics.Bitmap\nimp"
},
{
"path": "app/shared/src/main/kotlin/de/schnettler/datastore/manager/DataStoreManager.kt",
"chars": 1231,
"preview": "package de.schnettler.datastore.manager\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.coll"
},
{
"path": "app/shared/src/main/kotlin/de/schnettler/datastore/manager/PreferenceRequest.kt",
"chars": 189,
"preview": "package de.schnettler.datastore.manager\n\nimport androidx.datastore.preferences.core.Preferences\n\nopen class PreferenceRe"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/BVApp.kt",
"chars": 6127,
"preview": "package dev.aaa1115910.bv\n\nimport android.annotation.SuppressLint\nimport android.app.Application\nimport android.content."
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/activities/LauncherActivity.kt",
"chars": 2007,
"preview": "package dev.aaa1115910.bv.activities\n\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Bun"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/component/BvPlayerPreview.kt",
"chars": 2512,
"preview": "package dev.aaa1115910.bv.component\n\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.runti"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/component/DevelopingTip.kt",
"chars": 1662,
"preview": "package dev.aaa1115910.bv.component\n\nimport android.content.res.Configuration\nimport androidx.compose.foundation.layout."
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/component/FpsMonitor.kt",
"chars": 1700,
"preview": "package dev.aaa1115910.bv.component\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.la"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/component/QrImage.kt",
"chars": 5213,
"preview": "package dev.aaa1115910.bv.component\n\nimport android.graphics.BitmapFactory\nimport androidx.compose.foundation.Image\nimpo"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/component/settings/UpdateDialog.kt",
"chars": 17670,
"preview": "package dev.aaa1115910.bv.component.settings\n\nimport android.annotation.SuppressLint\nimport android.content.Intent\nimpor"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/dao/AppDatabase.kt",
"chars": 2003,
"preview": "package dev.aaa1115910.bv.dao\n\nimport android.content.Context\nimport androidx.room.AutoMigration\nimport androidx.room.Da"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/dao/SearchHistoryDao.kt",
"chars": 1107,
"preview": "package dev.aaa1115910.bv.dao\n\nimport androidx.room.Dao\nimport androidx.room.Delete\nimport androidx.room.Insert\nimport a"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/dao/UserDao.kt",
"chars": 587,
"preview": "package dev.aaa1115910.bv.dao\n\nimport androidx.room.Dao\nimport androidx.room.Delete\nimport androidx.room.Insert\nimport a"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/entity/AuthData.kt",
"chars": 1622,
"preview": "package dev.aaa1115910.bv.entity\n\nimport dev.aaa1115910.bv.util.Prefs\nimport kotlinx.serialization.SerialName\nimport kot"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/entity/BvScheme.kt",
"chars": 1499,
"preview": "package dev.aaa1115910.bv.entity\n\nimport android.net.Uri\n\nsealed class BvScheme(open val host: String) {\n companion o"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/entity/InterfaceMode.kt",
"chars": 384,
"preview": "package dev.aaa1115910.bv.entity\n\nimport android.content.Context\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.uti"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/entity/NavSwitchMode.kt",
"chars": 353,
"preview": "package dev.aaa1115910.bv.entity\n\nimport android.content.Context\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.uti"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/entity/PlayerType.kt",
"chars": 70,
"preview": "package dev.aaa1115910.bv.entity\n\nenum class PlayerType {\n Media3\n}"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/entity/ThemeType.kt",
"chars": 370,
"preview": "package dev.aaa1115910.bv.entity\n\nimport android.content.Context\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.uti"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/entity/carddata/SeasonCardData.kt",
"chars": 1269,
"preview": "package dev.aaa1115910.bv.entity.carddata\n\nimport dev.aaa1115910.biliapi.http.entity.search.SearchMediaResult\nimport dev"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/entity/carddata/VideoCardData.kt",
"chars": 1367,
"preview": "package dev.aaa1115910.bv.entity.carddata\n\nimport dev.aaa1115910.bv.util.formatHourMinSec\nimport java.text.SimpleDateFor"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/entity/db/SearchHistoryDB.kt",
"chars": 402,
"preview": "package dev.aaa1115910.bv.entity.db\n\nimport androidx.room.ColumnInfo\nimport androidx.room.Entity\nimport androidx.room.Pr"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/entity/db/UserDB.kt",
"chars": 514,
"preview": "package dev.aaa1115910.bv.entity.db\n\nimport androidx.room.ColumnInfo\nimport androidx.room.Entity\nimport androidx.room.Pr"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/entity/proxy/ProxyArea.kt",
"chars": 732,
"preview": "package dev.aaa1115910.bv.entity.proxy\n\nimport dev.aaa1115910.bv.util.Prefs\nimport io.github.oshai.kotlinlogging.KotlinL"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/network/GithubApi.kt",
"chars": 10901,
"preview": "package dev.aaa1115910.bv.network\n\nimport dev.aaa1115910.bv.BuildConfig\nimport dev.aaa1115910.bv.network.entity.Release\n"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/network/HttpServer.kt",
"chars": 2378,
"preview": "package dev.aaa1115910.bv.network\n\nimport dev.aaa1115910.bv.util.LogCatcherUtil\nimport io.ktor.http.ContentDisposition\ni"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/network/VlcLibsApi.kt",
"chars": 3292,
"preview": "package dev.aaa1115910.bv.network\n\nimport android.os.Build\nimport dev.aaa1115910.bv.network.entity.Release\nimport io.kto"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/network/entity/GithubRelease.kt",
"chars": 3074,
"preview": "package dev.aaa1115910.bv.network.entity\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializa"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/player/entity/DefaultSubtitle.kt",
"chars": 448,
"preview": "package dev.aaa1115910.bv.player.entity\n\n/**\n * 默认字幕语言\n */\nenum class DefaultSubtitle(val value: Int) {\n /** 关闭 */\n "
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/player/entity/NextVideoStrategy.kt",
"chars": 851,
"preview": "package dev.aaa1115910.bv.player.entity\n\nimport android.content.Context\n\nenum class NextVideoStrategy(val ordinalValue: "
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/player/entity/PlayerDefaultStartPosition.kt",
"chars": 942,
"preview": "package dev.aaa1115910.bv.player.entity\n\nimport android.content.Context\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/player/entity/PlayerLoadNextAction.kt",
"chars": 996,
"preview": "package dev.aaa1115910.bv.player.entity\n\nimport android.content.Context\nimport dev.aaa1115910.bv.R\n\n/**\n * 播放完成后加载下一个的处理"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/repository/UserRepository.kt",
"chars": 5730,
"preview": "package dev.aaa1115910.bv.repository\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableLo"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/repository/VideoInfoRepository.kt",
"chars": 821,
"preview": "package dev.aaa1115910.bv.repository\n\nimport dev.aaa1115910.biliapi.entity.video.Tag\nimport dev.aaa1115910.bv.entity.car"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/ui/theme/Theme.kt",
"chars": 4318,
"preview": "package dev.aaa1115910.bv.ui.theme\n\nimport android.app.Activity\nimport android.os.Build\nimport androidx.compose.foundati"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/ui/theme/Typography.kt",
"chars": 2988,
"preview": "package dev.aaa1115910.bv.ui.theme\n\nimport androidx.compose.ui.text.font.Font\nimport androidx.compose.ui.text.font.FontF"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/util/AbiUtil.kt",
"chars": 1350,
"preview": "package dev.aaa1115910.bv.util\n\nimport dev.aaa1115910.bv.BVApp\nimport java.io.File\nimport java.util.zip.ZipEntry\nimport "
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/util/BlacklistUtil.kt",
"chars": 3343,
"preview": "package dev.aaa1115910.bv.util\n\nimport android.content.Context\nimport dev.aaa1115910.bv.BVApp\nimport dev.aaa1115910.bv.B"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/util/CodecUtil.kt",
"chars": 6254,
"preview": "package dev.aaa1115910.bv.util\n\nimport android.media.MediaCodecInfo\nimport android.media.MediaCodecList\nimport android.o"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/util/CoilConfig.kt",
"chars": 2236,
"preview": "package dev.aaa1115910.bv.util\n\nimport android.content.Context\nimport android.os.Build\nimport coil.ImageLoader\nimport co"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/util/DanmakuRateLimiter.kt",
"chars": 2247,
"preview": "package dev.aaa1115910.bv.util\n\nimport dev.aaa1115910.biliapi.http.entity.live.DanmakuEvent\nimport kotlinx.coroutines.Co"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/util/DeviceUtil.kt",
"chars": 1367,
"preview": "package dev.aaa1115910.bv.util\n\nimport android.app.UiModeManager\nimport android.content.Context\nimport android.content.p"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/util/EnumExtends.kt",
"chars": 946,
"preview": "package dev.aaa1115910.bv.util\n\nimport android.content.Context\nimport dev.aaa1115910.biliapi.entity.season.FollowingSeas"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/util/Extends.kt",
"chars": 7568,
"preview": "package dev.aaa1115910.bv.util\n\nimport android.content.Context\nimport androidx.compose.foundation.lazy.LazyListState\nimp"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/util/LiveStreamUrlFetcher.kt",
"chars": 11190,
"preview": "package dev.aaa1115910.bv.util\n\nimport android.widget.Toast\nimport dev.aaa1115910.biliapi.entity.live.LiveCodec\nimport d"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/util/LogCatcherUtil.kt",
"chars": 5419,
"preview": "package dev.aaa1115910.bv.util\n\nimport dev.aaa1115910.bv.BVApp\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/util/ModifierExtends.kt",
"chars": 2898,
"preview": "package dev.aaa1115910.bv.util\n\nimport androidx.compose.animation.animateColor\nimport androidx.compose.animation.core.Li"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/util/NetworkUtil.kt",
"chars": 1839,
"preview": "package dev.aaa1115910.bv.util\n\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport io.ktor.client.HttpClient\nimpo"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/util/NotYetImplemented.kt",
"chars": 138,
"preview": "package dev.aaa1115910.bv.util\n\nimport dev.aaa1115910.bv.BVApp\n\nfun notYetImplemented() {\n \"Not yet implemented\".toas"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/util/PartitionUtil.kt",
"chars": 7792,
"preview": "package dev.aaa1115910.bv.util\n\nobject PartitionUtil {\n val partitions = listOf(\n Partition(\n 1, \"d"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/util/PgcIndexParamExtends.kt",
"chars": 2490,
"preview": "package dev.aaa1115910.bv.util\n\nimport android.content.Context\nimport dev.aaa1115910.biliapi.entity.pgc.index.Area\nimpor"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/util/PgcTypeExtends.kt",
"chars": 509,
"preview": "package dev.aaa1115910.bv.util\n\nimport android.content.Context\nimport dev.aaa1115910.biliapi.entity.pgc.PgcType\nimport d"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/util/Prefs.kt",
"chars": 38550,
"preview": "@file:Suppress(\"SpellCheckingInspection\")\n\npackage dev.aaa1115910.bv.util\n\nimport androidx.compose.ui.unit.Dp\nimport and"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/util/UgcTypeExtends.kt",
"chars": 7760,
"preview": "package dev.aaa1115910.bv.util\n\nimport android.content.Context\nimport dev.aaa1115910.biliapi.entity.ugc.UgcType\nimport d"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/util/UgcTypeV2Extends.kt",
"chars": 15620,
"preview": "package dev.aaa1115910.bv.util\n\nimport android.content.Context\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/util/calculateWindowSizeClassInPreview.kt",
"chars": 663,
"preview": "package dev.aaa1115910.bv.util\n\nimport androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassAp"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/CommentViewModel.kt",
"chars": 5746,
"preview": "package dev.aaa1115910.bv.viewmodel\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableInt"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/DynamicDetailViewModel.kt",
"chars": 1475,
"preview": "package dev.aaa1115910.bv.viewmodel\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableSta"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/SeasonViewModel.kt",
"chars": 4448,
"preview": "package dev.aaa1115910.bv.viewmodel\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableSta"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/TagViewModel.kt",
"chars": 2792,
"preview": "package dev.aaa1115910.bv.viewmodel\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableInt"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/UserSwitchViewModel.kt",
"chars": 1778,
"preview": "package dev.aaa1115910.bv.viewmodel\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableSta"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/UserViewModel.kt",
"chars": 3008,
"preview": "package dev.aaa1115910.bv.viewmodel\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableSta"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/VideoPlayerV3ViewModel.kt",
"chars": 74011,
"preview": "package dev.aaa1115910.bv.viewmodel\n\nimport android.net.Uri\nimport androidx.compose.runtime.getValue\nimport androidx.com"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/home/DynamicViewModel.kt",
"chars": 6117,
"preview": "package dev.aaa1115910.bv.viewmodel.home\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutab"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/home/PopularViewModel.kt",
"chars": 2423,
"preview": "package dev.aaa1115910.bv.viewmodel.home\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutab"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/home/RecommendViewModel.kt",
"chars": 3014,
"preview": "package dev.aaa1115910.bv.viewmodel.home\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutab"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/index/PgcIndexViewModel.kt",
"chars": 5183,
"preview": "package dev.aaa1115910.bv.viewmodel.index\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.muta"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/live/LiveViewModel.kt",
"chars": 10553,
"preview": "package dev.aaa1115910.bv.viewmodel.live\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutab"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/login/AppQrLoginViewModel.kt",
"chars": 5496,
"preview": "package dev.aaa1115910.bv.viewmodel.login\n\nimport android.graphics.BitmapFactory\nimport androidx.compose.runtime.getValu"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/login/SmsLoginViewModel.kt",
"chars": 6502,
"preview": "package dev.aaa1115910.bv.viewmodel.login\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.muta"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcAnimeViewModel.kt",
"chars": 365,
"preview": "package dev.aaa1115910.bv.viewmodel.pgc\n\nimport dev.aaa1115910.biliapi.entity.pgc.PgcType\nimport dev.aaa1115910.biliapi."
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcDocumentaryViewModel.kt",
"chars": 377,
"preview": "package dev.aaa1115910.bv.viewmodel.pgc\n\nimport dev.aaa1115910.biliapi.entity.pgc.PgcType\nimport dev.aaa1115910.biliapi."
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcGuoChuangViewModel.kt",
"chars": 373,
"preview": "package dev.aaa1115910.bv.viewmodel.pgc\n\nimport dev.aaa1115910.biliapi.entity.pgc.PgcType\nimport dev.aaa1115910.biliapi."
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcMovieViewModel.kt",
"chars": 365,
"preview": "package dev.aaa1115910.bv.viewmodel.pgc\n\nimport dev.aaa1115910.biliapi.entity.pgc.PgcType\nimport dev.aaa1115910.biliapi."
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcTvViewModel.kt",
"chars": 359,
"preview": "package dev.aaa1115910.bv.viewmodel.pgc\n\nimport dev.aaa1115910.biliapi.entity.pgc.PgcType\nimport dev.aaa1115910.biliapi."
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcVarietyViewModel.kt",
"chars": 369,
"preview": "package dev.aaa1115910.bv.viewmodel.pgc\n\nimport dev.aaa1115910.biliapi.entity.pgc.PgcType\nimport dev.aaa1115910.biliapi."
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcViewModel.kt",
"chars": 5604,
"preview": "package dev.aaa1115910.bv.viewmodel.pgc\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutabl"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/search/SearchInputViewModel.kt",
"chars": 5171,
"preview": "package dev.aaa1115910.bv.viewmodel.search\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mut"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/search/SearchResultViewModel.kt",
"chars": 8889,
"preview": "package dev.aaa1115910.bv.viewmodel.search\n\nimport android.content.Context\nimport androidx.compose.runtime.getValue\nimpo"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcAiViewModel.kt",
"chars": 363,
"preview": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliap"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcAnimalViewModel.kt",
"chars": 371,
"preview": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliap"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcCarViewModel.kt",
"chars": 365,
"preview": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliap"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcCinephileViewModel.kt",
"chars": 377,
"preview": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliap"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcDanceViewModel.kt",
"chars": 369,
"preview": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliap"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcDougaViewModel.kt",
"chars": 369,
"preview": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliap"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcEmotionViewModel.kt",
"chars": 373,
"preview": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliap"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcEntViewModel.kt",
"chars": 365,
"preview": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliap"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcFashionViewModel.kt",
"chars": 373,
"preview": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliap"
},
{
"path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcFoodViewModel.kt",
"chars": 367,
"preview": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliap"
}
]
// ... and 745 more files (download for full content)
About this extraction
This page contains the full source code of the fantasytyx/bv GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 945 files (4.8 MB), approximately 1.3M tokens, and a symbol index with 7 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.