Repository: Android-Builds/barinsta
Branch: master
Commit: 52d53c5ad180
Files: 1055
Total size: 4.2 MB
Directory structure:
gitextract_v9zl8srd/
├── .all-contributorsrc
├── .codebeatsettings
├── .github/
│ ├── CODE_OF_CONDUCT.md
│ ├── CONTRIBUTING.md
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── ban_report.md
│ │ ├── bug_report.md
│ │ ├── config.yml
│ │ └── feature_request.md
│ ├── issue_label_bot.yaml
│ └── workflows/
│ ├── github_nightly_release.yml
│ ├── github_pre_release.yml
│ ├── label-bugs.yml
│ └── label-duplicates.yml
├── .gitignore
├── .idea/
│ ├── .gitignore
│ ├── .name
│ ├── codeStyles
│ ├── compiler.xml
│ ├── gradle.xml
│ ├── inspectionProfiles/
│ │ └── profiles_settings.xml
│ ├── jarRepositories.xml
│ ├── misc.xml
│ ├── render.experimental.xml
│ ├── runConfigurations/
│ │ └── app.xml
│ ├── runConfigurations.xml
│ └── vcs.xml
├── .project
├── .settings/
│ └── org.eclipse.buildship.core.prefs
├── CHANGELOG
├── LICENSE
├── README.md
├── SECURITY.md
├── app/
│ ├── .classpath
│ ├── .gitignore
│ ├── .project
│ ├── .settings/
│ │ └── org.eclipse.buildship.core.prefs
│ ├── build.gradle
│ ├── lint.xml
│ ├── proguard-rules.pro
│ ├── schemas/
│ │ └── awais.instagrabber.db.AppDatabase/
│ │ ├── 4.json
│ │ ├── 5.json
│ │ └── 6.json
│ ├── sentry.gradle
│ └── src/
│ ├── androidTest/
│ │ └── java/
│ │ ├── awais/
│ │ │ └── instagrabber/
│ │ │ └── db/
│ │ │ ├── MigrationTest.java
│ │ │ └── dao/
│ │ │ └── RecentSearchDaoTest.kt
│ │ └── awaisomereport/
│ │ └── CrashReporterHelperTest.kt
│ ├── fdroid/
│ │ └── java/
│ │ ├── awais/
│ │ │ └── instagrabber/
│ │ │ ├── fragments/
│ │ │ │ └── settings/
│ │ │ │ └── FlavorSettings.java
│ │ │ └── utils/
│ │ │ └── UpdateChecker.java
│ │ └── awaisomereport/
│ │ └── CrashHandler.kt
│ ├── github/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ ├── awais/
│ │ │ │ └── instagrabber/
│ │ │ │ ├── fragments/
│ │ │ │ │ └── settings/
│ │ │ │ │ └── FlavorSettings.java
│ │ │ │ └── utils/
│ │ │ │ └── UpdateChecker.java
│ │ │ └── awaisomereport/
│ │ │ └── CrashHandler.kt
│ │ └── res/
│ │ ├── values/
│ │ │ └── strings.xml
│ │ ├── values-ar/
│ │ │ └── strings.xml
│ │ ├── values-ca/
│ │ │ └── strings.xml
│ │ ├── values-cs/
│ │ │ └── strings.xml
│ │ ├── values-de/
│ │ │ └── strings.xml
│ │ ├── values-el/
│ │ │ └── strings.xml
│ │ ├── values-es/
│ │ │ └── strings.xml
│ │ ├── values-eu/
│ │ │ └── strings.xml
│ │ ├── values-fa/
│ │ │ └── strings.xml
│ │ ├── values-fr/
│ │ │ └── strings.xml
│ │ ├── values-hi/
│ │ │ └── strings.xml
│ │ ├── values-in/
│ │ │ └── strings.xml
│ │ ├── values-it/
│ │ │ └── strings.xml
│ │ ├── values-ja/
│ │ │ └── strings.xml
│ │ ├── values-ko/
│ │ │ └── strings.xml
│ │ ├── values-mk/
│ │ │ └── strings.xml
│ │ ├── values-nl/
│ │ │ └── strings.xml
│ │ ├── values-or/
│ │ │ └── strings.xml
│ │ ├── values-pl/
│ │ │ └── strings.xml
│ │ ├── values-pt/
│ │ │ └── strings.xml
│ │ ├── values-ru/
│ │ │ └── strings.xml
│ │ ├── values-sk/
│ │ │ └── strings.xml
│ │ ├── values-sv/
│ │ │ └── strings.xml
│ │ ├── values-tr/
│ │ │ └── strings.xml
│ │ ├── values-vi/
│ │ │ └── strings.xml
│ │ ├── values-zh-rCN/
│ │ │ └── strings.xml
│ │ └── values-zh-rTW/
│ │ └── strings.xml
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ ├── awais/
│ │ │ │ └── instagrabber/
│ │ │ │ ├── InstaGrabberApplication.kt
│ │ │ │ ├── activities/
│ │ │ │ │ ├── BaseLanguageActivity.kt
│ │ │ │ │ ├── CameraActivity.kt
│ │ │ │ │ ├── DirectorySelectActivity.kt
│ │ │ │ │ ├── Login.kt
│ │ │ │ │ └── MainActivity.kt
│ │ │ │ ├── adapters/
│ │ │ │ │ ├── AccountSwitcherAdapter.java
│ │ │ │ │ ├── CommentsAdapter.java
│ │ │ │ │ ├── DirectItemsAdapter.java
│ │ │ │ │ ├── DirectMessageInboxAdapter.java
│ │ │ │ │ ├── DirectPendingUsersAdapter.java
│ │ │ │ │ ├── DirectReactionsAdapter.java
│ │ │ │ │ ├── DirectUsersAdapter.java
│ │ │ │ │ ├── DirectoryFilesAdapter.java
│ │ │ │ │ ├── DiscoverTopicsAdapter.java
│ │ │ │ │ ├── FavoritesAdapter.java
│ │ │ │ │ ├── FeedAdapterV2.java
│ │ │ │ │ ├── FeedItemCallbackAdapter.java
│ │ │ │ │ ├── FeedStoriesAdapter.java
│ │ │ │ │ ├── FeedStoriesListAdapter.java
│ │ │ │ │ ├── FiltersAdapter.java
│ │ │ │ │ ├── FollowAdapter.java
│ │ │ │ │ ├── GifItemsAdapter.java
│ │ │ │ │ ├── HighlightStoriesListAdapter.java
│ │ │ │ │ ├── HighlightsAdapter.java
│ │ │ │ │ ├── KeywordsFilterAdapter.java
│ │ │ │ │ ├── LikesAdapter.java
│ │ │ │ │ ├── NotificationsAdapter.java
│ │ │ │ │ ├── SavedCollectionsAdapter.java
│ │ │ │ │ ├── SearchCategoryAdapter.java
│ │ │ │ │ ├── SearchItemsAdapter.java
│ │ │ │ │ ├── SliderCallbackAdapter.java
│ │ │ │ │ ├── SliderItemsAdapter.java
│ │ │ │ │ ├── StoriesAdapter.java
│ │ │ │ │ ├── TabsAdapter.java
│ │ │ │ │ ├── UserSearchResultsAdapter.java
│ │ │ │ │ └── viewholder/
│ │ │ │ │ ├── CommentViewHolder.java
│ │ │ │ │ ├── DiscoverViewHolder.java
│ │ │ │ │ ├── FavoriteViewHolder.java
│ │ │ │ │ ├── FeedGridItemViewHolder.java
│ │ │ │ │ ├── FeedStoryViewHolder.java
│ │ │ │ │ ├── FilterViewHolder.java
│ │ │ │ │ ├── FollowsViewHolder.java
│ │ │ │ │ ├── HighlightViewHolder.java
│ │ │ │ │ ├── NotificationViewHolder.java
│ │ │ │ │ ├── SearchItemViewHolder.java
│ │ │ │ │ ├── SliderItemViewHolder.java
│ │ │ │ │ ├── SliderPhotoViewHolder.java
│ │ │ │ │ ├── SliderVideoViewHolder.java
│ │ │ │ │ ├── StoryListViewHolder.java
│ │ │ │ │ ├── TabViewHolder.java
│ │ │ │ │ ├── TopicClusterViewHolder.java
│ │ │ │ │ ├── dialogs/
│ │ │ │ │ │ └── KeywordsFilterDialogViewHolder.java
│ │ │ │ │ ├── directmessages/
│ │ │ │ │ │ ├── DirectInboxItemViewHolder.java
│ │ │ │ │ │ ├── DirectItemActionLogViewHolder.java
│ │ │ │ │ │ ├── DirectItemAnimatedMediaViewHolder.java
│ │ │ │ │ │ ├── DirectItemDefaultViewHolder.java
│ │ │ │ │ │ ├── DirectItemLikeViewHolder.java
│ │ │ │ │ │ ├── DirectItemLinkViewHolder.java
│ │ │ │ │ │ ├── DirectItemMediaShareViewHolder.java
│ │ │ │ │ │ ├── DirectItemMediaViewHolder.java
│ │ │ │ │ │ ├── DirectItemPlaceholderViewHolder.java
│ │ │ │ │ │ ├── DirectItemProfileViewHolder.java
│ │ │ │ │ │ ├── DirectItemRavenMediaViewHolder.java
│ │ │ │ │ │ ├── DirectItemReelShareViewHolder.java
│ │ │ │ │ │ ├── DirectItemStoryShareViewHolder.java
│ │ │ │ │ │ ├── DirectItemTextViewHolder.java
│ │ │ │ │ │ ├── DirectItemVideoCallEventViewHolder.java
│ │ │ │ │ │ ├── DirectItemViewHolder.java
│ │ │ │ │ │ ├── DirectItemVoiceMediaViewHolder.java
│ │ │ │ │ │ ├── DirectItemXmaViewHolder.java
│ │ │ │ │ │ ├── DirectPendingUserViewHolder.java
│ │ │ │ │ │ ├── DirectReactionViewHolder.java
│ │ │ │ │ │ ├── DirectUserViewHolder.java
│ │ │ │ │ │ └── RecipientThreadViewHolder.java
│ │ │ │ │ └── feed/
│ │ │ │ │ ├── FeedItemViewHolder.java
│ │ │ │ │ ├── FeedPhotoViewHolder.java
│ │ │ │ │ ├── FeedSliderViewHolder.java
│ │ │ │ │ └── FeedVideoViewHolder.java
│ │ │ │ ├── animations/
│ │ │ │ │ ├── CubicBezierInterpolator.java
│ │ │ │ │ ├── FabAnimation.java
│ │ │ │ │ ├── ResizeAnimation.java
│ │ │ │ │ ├── RevealOutlineAnimation.java
│ │ │ │ │ ├── RoundedRectRevealOutlineProvider.java
│ │ │ │ │ └── ScaleAnimation.java
│ │ │ │ ├── asyncs/
│ │ │ │ │ ├── DiscoverPostFetchService.java
│ │ │ │ │ ├── FeedPostFetchService.java
│ │ │ │ │ ├── HashtagPostFetchService.java
│ │ │ │ │ ├── LocationPostFetchService.java
│ │ │ │ │ ├── ProfilePostFetchService.java
│ │ │ │ │ └── SavedPostFetchService.java
│ │ │ │ ├── backup/
│ │ │ │ │ └── BarinstaBackupAgent.kt
│ │ │ │ ├── broadcasts/
│ │ │ │ │ └── DMRefreshBroadcastReceiver.java
│ │ │ │ ├── customviews/
│ │ │ │ │ ├── BarinstaFragmentNavigator.kt
│ │ │ │ │ ├── BarinstaNavHostFragment.kt
│ │ │ │ │ ├── ChatMessageLayout.java
│ │ │ │ │ ├── CircularImageView.java
│ │ │ │ │ ├── CommentMentionClickSpan.java
│ │ │ │ │ ├── DirectItemContextMenu.java
│ │ │ │ │ ├── DirectItemFrameLayout.java
│ │ │ │ │ ├── FixedImageView.java
│ │ │ │ │ ├── FormattedNumberTextView.java
│ │ │ │ │ ├── InsetsAnimationLinearLayout.java
│ │ │ │ │ ├── InsetsNotifyingCoordinatorLayout.java
│ │ │ │ │ ├── InsetsNotifyingLinearLayout.java
│ │ │ │ │ ├── KeyNotifyingEmojiEditText.java
│ │ │ │ │ ├── MouseDrawer.java
│ │ │ │ │ ├── PostsRecyclerView.java
│ │ │ │ │ ├── PrimaryActionModeCallback.java
│ │ │ │ │ ├── ProfilePicView.java
│ │ │ │ │ ├── RamboTextViewV2.java
│ │ │ │ │ ├── ReactionEmojiTextView.java
│ │ │ │ │ ├── RecordButton.java
│ │ │ │ │ ├── RecordView.java
│ │ │ │ │ ├── SharedElementTransitionDialogFragment.java
│ │ │ │ │ ├── SquareImageView.java
│ │ │ │ │ ├── TextViewDrawableSize.java
│ │ │ │ │ ├── Tooltip.java
│ │ │ │ │ ├── UsernameTextView.java
│ │ │ │ │ ├── VerticalDragHelper.java
│ │ │ │ │ ├── VerticalImageSpan.java
│ │ │ │ │ ├── VideoPlayerCallbackAdapter.java
│ │ │ │ │ ├── VideoPlayerViewHelper.java
│ │ │ │ │ ├── drawee/
│ │ │ │ │ │ ├── AbstractAnimatedZoomableController.java
│ │ │ │ │ │ ├── AnimatedZoomableController.java
│ │ │ │ │ │ ├── DefaultZoomableController.java
│ │ │ │ │ │ ├── DoubleTapGestureListener.java
│ │ │ │ │ │ ├── DraggableZoomableDraweeView.java
│ │ │ │ │ │ ├── GestureListenerWrapper.java
│ │ │ │ │ │ ├── MultiGestureListener.java
│ │ │ │ │ │ ├── MultiPointerGestureDetector.java
│ │ │ │ │ │ ├── MultiZoomableControllerListener.java
│ │ │ │ │ │ ├── TransformGestureDetector.java
│ │ │ │ │ │ ├── ZoomableController.java
│ │ │ │ │ │ └── ZoomableDraweeView.java
│ │ │ │ │ ├── emoji/
│ │ │ │ │ │ ├── Emoji.java
│ │ │ │ │ │ ├── EmojiBottomSheetDialog.java
│ │ │ │ │ │ ├── EmojiCategory.java
│ │ │ │ │ │ ├── EmojiCategoryPageViewHolder.java
│ │ │ │ │ │ ├── EmojiCategoryType.java
│ │ │ │ │ │ ├── EmojiGridAdapter.java
│ │ │ │ │ │ ├── EmojiPicker.java
│ │ │ │ │ │ ├── EmojiPickerPageAdapter.java
│ │ │ │ │ │ ├── EmojiVariantManager.java
│ │ │ │ │ │ ├── EmojiVariantPopup.java
│ │ │ │ │ │ ├── GoogleCompatEmojiDrawable.java
│ │ │ │ │ │ └── ReactionsManager.java
│ │ │ │ │ ├── helpers/
│ │ │ │ │ │ ├── ChangeText.java
│ │ │ │ │ │ ├── ControlFocusInsetsAnimationCallback.java
│ │ │ │ │ │ ├── CustomHideBottomViewOnScrollBehavior.java
│ │ │ │ │ │ ├── EmojiPickerInsetsAnimationCallback.java
│ │ │ │ │ │ ├── GridAutofitLayoutManager.java
│ │ │ │ │ │ ├── GridSpacingItemDecoration.java
│ │ │ │ │ │ ├── HeaderItemDecoration.java
│ │ │ │ │ │ ├── HeightProvider.java
│ │ │ │ │ │ ├── ImageResizingControllerListener.java
│ │ │ │ │ │ ├── NestedCoordinatorLayout.java
│ │ │ │ │ │ ├── NestedScrollableHost.java
│ │ │ │ │ │ ├── PostFetcher.java
│ │ │ │ │ │ ├── RecordViewAnimationHelper.java
│ │ │ │ │ │ ├── RecyclerLazyLoader.java
│ │ │ │ │ │ ├── RecyclerLazyLoaderAtEdge.java
│ │ │ │ │ │ ├── RootViewDeferringInsetsCallback.java
│ │ │ │ │ │ ├── SimpleImeAnimationController.java
│ │ │ │ │ │ ├── SwipeAndRestoreItemTouchHelperCallback.java
│ │ │ │ │ │ ├── SwipeGestureListener.java
│ │ │ │ │ │ ├── TextWatcherAdapter.java
│ │ │ │ │ │ ├── TranslateDeferringInsetsAnimationCallback.java
│ │ │ │ │ │ ├── VerticalSpaceItemDecoration.java
│ │ │ │ │ │ └── VideoAwareRecyclerScroller.java
│ │ │ │ │ └── masoudss_waveform/
│ │ │ │ │ ├── SoundParser.java
│ │ │ │ │ ├── WaveFormProgressChangeListener.java
│ │ │ │ │ ├── WaveGravity.java
│ │ │ │ │ └── WaveformSeekBar.java
│ │ │ │ ├── db/
│ │ │ │ │ ├── AppDatabase.kt
│ │ │ │ │ ├── Converters.kt
│ │ │ │ │ ├── dao/
│ │ │ │ │ │ ├── AccountDao.kt
│ │ │ │ │ │ ├── DMLastNotifiedDao.kt
│ │ │ │ │ │ ├── FavoriteDao.kt
│ │ │ │ │ │ └── RecentSearchDao.kt
│ │ │ │ │ ├── datasources/
│ │ │ │ │ │ ├── AccountDataSource.kt
│ │ │ │ │ │ ├── DMLastNotifiedDataSource.kt
│ │ │ │ │ │ ├── FavoriteDataSource.kt
│ │ │ │ │ │ └── RecentSearchDataSource.kt
│ │ │ │ │ ├── entities/
│ │ │ │ │ │ ├── Account.kt
│ │ │ │ │ │ ├── DMLastNotified.kt
│ │ │ │ │ │ ├── Favorite.kt
│ │ │ │ │ │ └── RecentSearch.kt
│ │ │ │ │ └── repositories/
│ │ │ │ │ ├── AccountRepository.kt
│ │ │ │ │ ├── DMLastNotifiedRepository.kt
│ │ │ │ │ ├── FavoriteRepository.kt
│ │ │ │ │ └── RecentSearchRepository.kt
│ │ │ │ ├── dialogs/
│ │ │ │ │ ├── AccountSwitcherDialogFragment.java
│ │ │ │ │ ├── ConfirmDialogFragment.java
│ │ │ │ │ ├── CreateBackupDialogFragment.java
│ │ │ │ │ ├── DirectItemReactionDialogFragment.java
│ │ │ │ │ ├── EditTextDialogFragment.java
│ │ │ │ │ ├── GifPickerBottomDialogFragment.java
│ │ │ │ │ ├── KeywordsFilterDialog.java
│ │ │ │ │ ├── MultiOptionDialogFragment.java
│ │ │ │ │ ├── PostLoadingDialogFragment.kt
│ │ │ │ │ ├── PostsLayoutPreferencesDialogFragment.kt
│ │ │ │ │ ├── ProfilePicDialogFragment.java
│ │ │ │ │ ├── RestoreBackupDialogFragment.java
│ │ │ │ │ ├── TabOrderPreferenceDialogFragment.java
│ │ │ │ │ └── TimeSettingsDialog.java
│ │ │ │ ├── fragments/
│ │ │ │ │ ├── CollectionPostsFragment.java
│ │ │ │ │ ├── FavoritesFragment.kt
│ │ │ │ │ ├── FollowViewerFragment.kt
│ │ │ │ │ ├── HashTagFragment.java
│ │ │ │ │ ├── LikesViewerFragment.java
│ │ │ │ │ ├── LocationFragment.java
│ │ │ │ │ ├── NotificationsViewerFragment.java
│ │ │ │ │ ├── PostViewV2Fragment.java
│ │ │ │ │ ├── SavedCollectionsFragment.java
│ │ │ │ │ ├── SavedViewerFragment.java
│ │ │ │ │ ├── StoryListViewerFragment.java
│ │ │ │ │ ├── StoryViewerFragment.kt
│ │ │ │ │ ├── UserSearchFragment.kt
│ │ │ │ │ ├── UserSearchMode.kt
│ │ │ │ │ ├── comments/
│ │ │ │ │ │ ├── CommentsViewerFragment.java
│ │ │ │ │ │ ├── Helper.java
│ │ │ │ │ │ └── RepliesFragment.java
│ │ │ │ │ ├── directmessages/
│ │ │ │ │ │ ├── DirectMessageInboxFragment.kt
│ │ │ │ │ │ ├── DirectMessageSettingsFragment.kt
│ │ │ │ │ │ ├── DirectMessageThreadFragment.java
│ │ │ │ │ │ └── DirectPendingInboxFragment.kt
│ │ │ │ │ ├── imageedit/
│ │ │ │ │ │ ├── FiltersFragment.java
│ │ │ │ │ │ ├── ImageEditFragment.java
│ │ │ │ │ │ └── filters/
│ │ │ │ │ │ ├── FiltersHelper.java
│ │ │ │ │ │ ├── custom/
│ │ │ │ │ │ │ ├── GPUImage1977Filter.java
│ │ │ │ │ │ │ ├── GPUImageAdenFilter.java
│ │ │ │ │ │ │ └── GPUImageClarendonFilter.java
│ │ │ │ │ │ ├── filters/
│ │ │ │ │ │ │ ├── AdenFilter.java
│ │ │ │ │ │ │ ├── BilateralBlurFilter.java
│ │ │ │ │ │ │ ├── BoxBlurFilter.java
│ │ │ │ │ │ │ ├── BrightnessFilter.java
│ │ │ │ │ │ │ ├── ClarendonFilter.java
│ │ │ │ │ │ │ ├── ContrastFilter.java
│ │ │ │ │ │ │ ├── ExposureFilter.java
│ │ │ │ │ │ │ ├── Filter.java
│ │ │ │ │ │ │ ├── FilterFactory.java
│ │ │ │ │ │ │ ├── NormalFilter.java
│ │ │ │ │ │ │ ├── One977Filter.java
│ │ │ │ │ │ │ ├── SaturationFilter.java
│ │ │ │ │ │ │ ├── SepiaToneFilter.java
│ │ │ │ │ │ │ ├── SharpenFilter.java
│ │ │ │ │ │ │ ├── VibranceFilter.java
│ │ │ │ │ │ │ └── VignetteFilter.java
│ │ │ │ │ │ └── properties/
│ │ │ │ │ │ ├── ColorProperty.java
│ │ │ │ │ │ ├── FloatProperty.java
│ │ │ │ │ │ ├── PointFProperty.java
│ │ │ │ │ │ └── Property.java
│ │ │ │ │ ├── main/
│ │ │ │ │ │ ├── DiscoverFragment.java
│ │ │ │ │ │ ├── FeedFragment.java
│ │ │ │ │ │ └── ProfileFragment.kt
│ │ │ │ │ ├── search/
│ │ │ │ │ │ ├── SearchCategoryFragment.java
│ │ │ │ │ │ └── SearchFragment.java
│ │ │ │ │ └── settings/
│ │ │ │ │ ├── AboutFragment.java
│ │ │ │ │ ├── BackupPreferencesFragment.java
│ │ │ │ │ ├── BasePreferencesFragment.java
│ │ │ │ │ ├── DMPreferencesFragment.java
│ │ │ │ │ ├── DownloadsPreferencesFragment.java
│ │ │ │ │ ├── GeneralPreferencesFragment.java
│ │ │ │ │ ├── IFlavorSettings.java
│ │ │ │ │ ├── LocalePreferencesFragment.java
│ │ │ │ │ ├── MorePreferencesFragment.java
│ │ │ │ │ ├── NotificationsPreferencesFragment.java
│ │ │ │ │ ├── PostPreferencesFragment.java
│ │ │ │ │ ├── PreferenceHelper.java
│ │ │ │ │ ├── PreferenceKeys.kt
│ │ │ │ │ ├── SettingCategory.java
│ │ │ │ │ ├── SettingsPreferencesFragment.java
│ │ │ │ │ ├── StoriesPreferencesFragment.java
│ │ │ │ │ └── ThemePreferencesFragment.java
│ │ │ │ ├── interfaces/
│ │ │ │ │ ├── FetchListener.java
│ │ │ │ │ ├── LazyLoadListener.java
│ │ │ │ │ ├── OnGroupClickListener.java
│ │ │ │ │ └── SwipeEvent.java
│ │ │ │ ├── managers/
│ │ │ │ │ ├── DirectMessagesManager.kt
│ │ │ │ │ ├── InboxManager.kt
│ │ │ │ │ └── ThreadManager.kt
│ │ │ │ ├── models/
│ │ │ │ │ ├── Comment.kt
│ │ │ │ │ ├── IntentModel.kt
│ │ │ │ │ ├── PostsLayoutPreferences.java
│ │ │ │ │ ├── Resource.kt
│ │ │ │ │ ├── SavedImageEditState.kt
│ │ │ │ │ ├── Tab.kt
│ │ │ │ │ ├── UploadPhotoOptions.kt
│ │ │ │ │ ├── UploadVideoOptions.kt
│ │ │ │ │ └── enums/
│ │ │ │ │ ├── BroadcastItemType.kt
│ │ │ │ │ ├── DirectItemType.kt
│ │ │ │ │ ├── FavoriteType.kt
│ │ │ │ │ ├── FollowingType.kt
│ │ │ │ │ ├── IntentModelType.kt
│ │ │ │ │ ├── MediaItemType.kt
│ │ │ │ │ ├── NotificationType.kt
│ │ │ │ │ ├── PostItemType.kt
│ │ │ │ │ ├── RavenMediaViewMode.kt
│ │ │ │ │ └── StoryPaginationType.kt
│ │ │ │ ├── repositories/
│ │ │ │ │ ├── CollectionRepository.java
│ │ │ │ │ ├── CommentRepository.java
│ │ │ │ │ ├── DirectMessagesService.kt
│ │ │ │ │ ├── DiscoverRepository.java
│ │ │ │ │ ├── FeedRepository.java
│ │ │ │ │ ├── FriendshipService.kt
│ │ │ │ │ ├── GifRepository.java
│ │ │ │ │ ├── GraphQLService.kt
│ │ │ │ │ ├── LocationRepository.java
│ │ │ │ │ ├── MediaService.kt
│ │ │ │ │ ├── NewsRepository.java
│ │ │ │ │ ├── ProfileService.kt
│ │ │ │ │ ├── SearchService.kt
│ │ │ │ │ ├── StoriesService.kt
│ │ │ │ │ ├── TagsRepository.java
│ │ │ │ │ ├── UserService.kt
│ │ │ │ │ ├── requests/
│ │ │ │ │ │ ├── StoryViewerOptions.java
│ │ │ │ │ │ ├── UploadFinishOptions.kt
│ │ │ │ │ │ └── directmessages/
│ │ │ │ │ │ ├── AnimatedMediaBroadcastOptions.kt
│ │ │ │ │ │ ├── BroadcastOptions.kt
│ │ │ │ │ │ ├── LinkBroadcastOptions.kt
│ │ │ │ │ │ ├── MediaShareBroadcastOptions.kt
│ │ │ │ │ │ ├── PhotoBroadcastOptions.kt
│ │ │ │ │ │ ├── ProfileBroadcastOptions.kt
│ │ │ │ │ │ ├── ReactionBroadcastOptions.kt
│ │ │ │ │ │ ├── StoryBroadcastOptions.kt
│ │ │ │ │ │ ├── StoryReplyBroadcastOptions.kt
│ │ │ │ │ │ ├── TextBroadcastOptions.kt
│ │ │ │ │ │ ├── ThreadIdsOrUserIds.kt
│ │ │ │ │ │ ├── VideoBroadcastOptions.kt
│ │ │ │ │ │ └── VoiceBroadcastOptions.kt
│ │ │ │ │ ├── responses/
│ │ │ │ │ │ ├── AnimatedMediaFixedHeight.kt
│ │ │ │ │ │ ├── AnimatedMediaImages.kt
│ │ │ │ │ │ ├── Audio.kt
│ │ │ │ │ │ ├── AymlResponse.kt
│ │ │ │ │ │ ├── Caption.kt
│ │ │ │ │ │ ├── ChildCommentsFetchResponse.kt
│ │ │ │ │ │ ├── CommentsFetchResponse.kt
│ │ │ │ │ │ ├── FriendshipChangeResponse.kt
│ │ │ │ │ │ ├── FriendshipListFetchResponse.kt
│ │ │ │ │ │ ├── FriendshipRestrictResponse.kt
│ │ │ │ │ │ ├── FriendshipStatus.kt
│ │ │ │ │ │ ├── GraphQLUserListFetchResponse.java
│ │ │ │ │ │ ├── Hashtag.kt
│ │ │ │ │ │ ├── ImageUrl.kt
│ │ │ │ │ │ ├── ImageVersions2.kt
│ │ │ │ │ │ ├── LikersResponse.kt
│ │ │ │ │ │ ├── Location.java
│ │ │ │ │ │ ├── LocationFeedResponse.kt
│ │ │ │ │ │ ├── Media.kt
│ │ │ │ │ │ ├── MediaCandidate.kt
│ │ │ │ │ │ ├── MediaInfoResponse.kt
│ │ │ │ │ │ ├── NewsInboxResponse.kt
│ │ │ │ │ │ ├── Place.kt
│ │ │ │ │ │ ├── PostsFetchResponse.kt
│ │ │ │ │ │ ├── TagFeedResponse.kt
│ │ │ │ │ │ ├── User.kt
│ │ │ │ │ │ ├── UserFeedResponse.kt
│ │ │ │ │ │ ├── UserProfileContextLink.kt
│ │ │ │ │ │ ├── UserSearchResponse.kt
│ │ │ │ │ │ ├── UsertagIn.java
│ │ │ │ │ │ ├── Usertags.java
│ │ │ │ │ │ ├── WrappedFeedResponse.java
│ │ │ │ │ │ ├── WrappedMedia.kt
│ │ │ │ │ │ ├── WrappedUser.kt
│ │ │ │ │ │ ├── directmessages/
│ │ │ │ │ │ │ ├── DirectBadgeCount.kt
│ │ │ │ │ │ │ ├── DirectInbox.kt
│ │ │ │ │ │ │ ├── DirectInboxResponse.kt
│ │ │ │ │ │ │ ├── DirectItem.kt
│ │ │ │ │ │ │ ├── DirectItemActionLog.kt
│ │ │ │ │ │ │ ├── DirectItemAnimatedMedia.kt
│ │ │ │ │ │ │ ├── DirectItemClip.kt
│ │ │ │ │ │ │ ├── DirectItemEmojiReaction.kt
│ │ │ │ │ │ │ ├── DirectItemFelixShare.kt
│ │ │ │ │ │ │ ├── DirectItemLink.kt
│ │ │ │ │ │ │ ├── DirectItemLinkContext.kt
│ │ │ │ │ │ │ ├── DirectItemPlaceholder.kt
│ │ │ │ │ │ │ ├── DirectItemReactions.kt
│ │ │ │ │ │ │ ├── DirectItemReelShare.kt
│ │ │ │ │ │ │ ├── DirectItemReelShareReactionInfo.kt
│ │ │ │ │ │ │ ├── DirectItemSeenResponse.kt
│ │ │ │ │ │ │ ├── DirectItemSeenResponsePayload.kt
│ │ │ │ │ │ │ ├── DirectItemStoryShare.kt
│ │ │ │ │ │ │ ├── DirectItemVideoCallEvent.kt
│ │ │ │ │ │ │ ├── DirectItemVisualMedia.kt
│ │ │ │ │ │ │ ├── DirectItemVoiceMedia.kt
│ │ │ │ │ │ │ ├── DirectItemXma.kt
│ │ │ │ │ │ │ ├── DirectThread.kt
│ │ │ │ │ │ │ ├── DirectThreadBroadcastResponse.kt
│ │ │ │ │ │ │ ├── DirectThreadBroadcastResponseMessageMetadata.kt
│ │ │ │ │ │ │ ├── DirectThreadBroadcastResponsePayload.kt
│ │ │ │ │ │ │ ├── DirectThreadDetailsChangeResponse.kt
│ │ │ │ │ │ │ ├── DirectThreadDirectStory.kt
│ │ │ │ │ │ │ ├── DirectThreadFeedResponse.kt
│ │ │ │ │ │ │ ├── DirectThreadLastSeenAt.kt
│ │ │ │ │ │ │ ├── DirectThreadParticipantRequestsResponse.kt
│ │ │ │ │ │ │ ├── RankedRecipient.kt
│ │ │ │ │ │ │ ├── RankedRecipientsResponse.kt
│ │ │ │ │ │ │ ├── RavenExpiringMediaActionSummary.kt
│ │ │ │ │ │ │ ├── TextRange.kt
│ │ │ │ │ │ │ └── ThreadContext.kt
│ │ │ │ │ │ ├── discover/
│ │ │ │ │ │ │ ├── TopicCluster.kt
│ │ │ │ │ │ │ └── TopicalExploreFeedResponse.kt
│ │ │ │ │ │ ├── feed/
│ │ │ │ │ │ │ ├── EndOfFeedDemarcator.java
│ │ │ │ │ │ │ ├── EndOfFeedGroup.java
│ │ │ │ │ │ │ ├── EndOfFeedGroupSet.java
│ │ │ │ │ │ │ └── FeedFetchResponse.java
│ │ │ │ │ │ ├── giphy/
│ │ │ │ │ │ │ ├── GiphyGif.java
│ │ │ │ │ │ │ ├── GiphyGifImage.java
│ │ │ │ │ │ │ ├── GiphyGifImages.java
│ │ │ │ │ │ │ ├── GiphyGifResponse.java
│ │ │ │ │ │ │ └── GiphyGifResults.java
│ │ │ │ │ │ ├── notification/
│ │ │ │ │ │ │ ├── Notification.kt
│ │ │ │ │ │ │ ├── NotificationArgs.java
│ │ │ │ │ │ │ ├── NotificationCounts.kt
│ │ │ │ │ │ │ └── NotificationImage.kt
│ │ │ │ │ │ ├── saved/
│ │ │ │ │ │ │ ├── CollectionsListResponse.kt
│ │ │ │ │ │ │ └── SavedCollection.kt
│ │ │ │ │ │ ├── search/
│ │ │ │ │ │ │ ├── SearchItem.java
│ │ │ │ │ │ │ └── SearchResponse.kt
│ │ │ │ │ │ └── stories/
│ │ │ │ │ │ ├── ArchiveResponse.kt
│ │ │ │ │ │ ├── Broadcast.kt
│ │ │ │ │ │ ├── CoverMedia.kt
│ │ │ │ │ │ ├── PollSticker.kt
│ │ │ │ │ │ ├── QuestionSticker.kt
│ │ │ │ │ │ ├── QuizSticker.kt
│ │ │ │ │ │ ├── ReelsMediaResponse.kt
│ │ │ │ │ │ ├── ReelsResponse.kt
│ │ │ │ │ │ ├── ReelsTrayResponse.kt
│ │ │ │ │ │ ├── SliderSticker.kt
│ │ │ │ │ │ ├── Story.kt
│ │ │ │ │ │ ├── StoryAppAttribution.kt
│ │ │ │ │ │ ├── StoryCta.kt
│ │ │ │ │ │ ├── StoryMedia.kt
│ │ │ │ │ │ ├── StoryMediaResponse.kt
│ │ │ │ │ │ ├── StorySticker.kt
│ │ │ │ │ │ ├── StoryStickerResponse.kt
│ │ │ │ │ │ └── Tally.kt
│ │ │ │ │ └── serializers/
│ │ │ │ │ └── CaptionDeserializer.java
│ │ │ │ ├── services/
│ │ │ │ │ ├── ActivityCheckerService.java
│ │ │ │ │ ├── BootCompletedReceiver.java
│ │ │ │ │ ├── DMSyncAlarmReceiver.java
│ │ │ │ │ ├── DMSyncService.java
│ │ │ │ │ └── DeleteImageIntentService.java
│ │ │ │ ├── utils/
│ │ │ │ │ ├── AppExecutors.kt
│ │ │ │ │ ├── BarinstaDeepLinkHelper.kt
│ │ │ │ │ ├── BitmapUtils.kt
│ │ │ │ │ ├── CombinedDrawable.kt
│ │ │ │ │ ├── ConcurrencyHelpers.kt
│ │ │ │ │ ├── Constants.kt
│ │ │ │ │ ├── CookieUtils.kt
│ │ │ │ │ ├── CoroutineUtils.kt
│ │ │ │ │ ├── CubicInterpolation.kt
│ │ │ │ │ ├── DMUtils.java
│ │ │ │ │ ├── DateUtils.kt
│ │ │ │ │ ├── Debouncer.java
│ │ │ │ │ ├── DeepLinkParser.kt
│ │ │ │ │ ├── DirectItemFactory.kt
│ │ │ │ │ ├── DownloadUtils.kt
│ │ │ │ │ ├── Event.kt
│ │ │ │ │ ├── ExoplayerUtils.kt
│ │ │ │ │ ├── ExportImportUtils.java
│ │ │ │ │ ├── FlavorTown.java
│ │ │ │ │ ├── IntentUtils.kt
│ │ │ │ │ ├── KeywordsFilterUtils.kt
│ │ │ │ │ ├── LocaleUtils.kt
│ │ │ │ │ ├── MediaUploadHelper.kt
│ │ │ │ │ ├── MediaUploader.kt
│ │ │ │ │ ├── MediaUtils.java
│ │ │ │ │ ├── NavigationExtensions.java
│ │ │ │ │ ├── NavigationHelper.kt
│ │ │ │ │ ├── NetworkUtils.java
│ │ │ │ │ ├── NullSafePair.kt
│ │ │ │ │ ├── NumberUtils.kt
│ │ │ │ │ ├── PasswordUtils.kt
│ │ │ │ │ ├── PermissionUtils.kt
│ │ │ │ │ ├── ProcessPhoenix.java
│ │ │ │ │ ├── RankedRecipientsCache.kt
│ │ │ │ │ ├── ResponseBodyUtils.java
│ │ │ │ │ ├── SerializablePair.kt
│ │ │ │ │ ├── SettingsHelper.kt
│ │ │ │ │ ├── SingleLiveEvent.kt
│ │ │ │ │ ├── SingletonHolder.kt
│ │ │ │ │ ├── TextUtils.kt
│ │ │ │ │ ├── ThemeUtils.kt
│ │ │ │ │ ├── UpdateCheckCommon.kt
│ │ │ │ │ ├── UserAgentUtils.kt
│ │ │ │ │ ├── Utils.java
│ │ │ │ │ ├── ViewUtils.kt
│ │ │ │ │ ├── VoiceRecorder.java
│ │ │ │ │ ├── emoji/
│ │ │ │ │ │ ├── EmojiCategoryDeserializer.kt
│ │ │ │ │ │ ├── EmojiDeserializer.kt
│ │ │ │ │ │ └── EmojiParser.kt
│ │ │ │ │ └── extensions/
│ │ │ │ │ ├── AnyExtensions.kt
│ │ │ │ │ ├── StringExtensions.kt
│ │ │ │ │ └── UserExtensions.kt
│ │ │ │ ├── viewmodels/
│ │ │ │ │ ├── AppStateViewModel.java
│ │ │ │ │ ├── ArchivesViewModel.java
│ │ │ │ │ ├── CommentsViewerViewModel.java
│ │ │ │ │ ├── DirectInboxViewModel.kt
│ │ │ │ │ ├── DirectPendingInboxViewModel.kt
│ │ │ │ │ ├── DirectSettingsViewModel.kt
│ │ │ │ │ ├── DirectThreadViewModel.kt
│ │ │ │ │ ├── DirectorySelectActivityViewModel.kt
│ │ │ │ │ ├── FavoritesViewModel.kt
│ │ │ │ │ ├── FeedStoriesViewModel.java
│ │ │ │ │ ├── FileListViewModel.java
│ │ │ │ │ ├── FiltersFragmentViewModel.java
│ │ │ │ │ ├── FollowViewModel.kt
│ │ │ │ │ ├── GifPickerViewModel.java
│ │ │ │ │ ├── ImageEditViewModel.java
│ │ │ │ │ ├── MediaViewModel.java
│ │ │ │ │ ├── NotificationViewModel.java
│ │ │ │ │ ├── PostViewV2ViewModel.kt
│ │ │ │ │ ├── ProfileFragmentViewModel.kt
│ │ │ │ │ ├── SavedCollectionsViewModel.java
│ │ │ │ │ ├── SearchFragmentViewModel.kt
│ │ │ │ │ ├── StoryFragmentViewModel.kt
│ │ │ │ │ ├── TopicClusterViewModel.java
│ │ │ │ │ ├── UserSearchViewModel.java
│ │ │ │ │ └── factories/
│ │ │ │ │ ├── DirectSettingsViewModelFactory.java
│ │ │ │ │ └── DirectThreadViewModelFactory.java
│ │ │ │ ├── webservices/
│ │ │ │ │ ├── CollectionService.java
│ │ │ │ │ ├── CommentService.java
│ │ │ │ │ ├── DirectMessagesRepository.kt
│ │ │ │ │ ├── DiscoverService.java
│ │ │ │ │ ├── FeedService.java
│ │ │ │ │ ├── FriendshipRepository.kt
│ │ │ │ │ ├── GifService.java
│ │ │ │ │ ├── GraphQLRepository.kt
│ │ │ │ │ ├── LocationService.java
│ │ │ │ │ ├── MediaRepository.kt
│ │ │ │ │ ├── NewsService.java
│ │ │ │ │ ├── ProfileRepository.kt
│ │ │ │ │ ├── RetrofitFactory.kt
│ │ │ │ │ ├── SearchRepository.kt
│ │ │ │ │ ├── ServiceCallback.java
│ │ │ │ │ ├── StoriesRepository.kt
│ │ │ │ │ ├── TagsService.java
│ │ │ │ │ ├── UserRepository.kt
│ │ │ │ │ └── interceptors/
│ │ │ │ │ ├── AddCookiesInterceptor.java
│ │ │ │ │ ├── IgErrorsInterceptor.java
│ │ │ │ │ └── LoggingInterceptor.java
│ │ │ │ └── workers/
│ │ │ │ └── DownloadWorker.kt
│ │ │ ├── awaisomereport/
│ │ │ │ ├── CrashReporter.kt
│ │ │ │ ├── CrashReporterHelper.kt
│ │ │ │ ├── ErrorReporterActivity.kt
│ │ │ │ └── ICrashHandler.kt
│ │ │ └── thoughtbot/
│ │ │ └── expandableadapter/
│ │ │ ├── ExpandableGroup.java
│ │ │ ├── ExpandableList.java
│ │ │ ├── ExpandableListPosition.java
│ │ │ └── GroupViewHolder.java
│ │ └── res/
│ │ ├── anim/
│ │ │ ├── dialog_anim_in.xml
│ │ │ ├── dialog_anim_out.xml
│ │ │ ├── slide_in_right.xml
│ │ │ ├── slide_left.xml
│ │ │ ├── slide_out_left.xml
│ │ │ └── slide_right.xml
│ │ ├── animator/
│ │ │ ├── basket_path.xml
│ │ │ └── delete_mic_animation.xml
│ │ ├── color/
│ │ │ ├── emoji_picker_tab_color.xml
│ │ │ ├── filter_name_color.xml
│ │ │ ├── ic_circle_check_tint.xml
│ │ │ ├── ic_read_button_tint.xml
│ │ │ └── image_edit_tab_tint.xml
│ │ ├── drawable/
│ │ │ ├── avd_mic_to_send_anim.xml
│ │ │ ├── avd_send_to_mic_anim.xml
│ │ │ ├── background_grey_ripple.xml
│ │ │ ├── bg_dm_date_header.xml
│ │ │ ├── bg_dm_time.xml
│ │ │ ├── bg_indicator.xml
│ │ │ ├── bg_input.xml
│ │ │ ├── bg_media_share_bottom.xml
│ │ │ ├── bg_media_share_top_incoming.xml
│ │ │ ├── bg_media_share_top_outgoing.xml
│ │ │ ├── bg_quote_line.xml
│ │ │ ├── bg_reply_text.xml
│ │ │ ├── bg_rounded_corner.xml
│ │ │ ├── bg_speech_bubble_incoming.xml
│ │ │ ├── bg_speech_bubble_outgoing.xml
│ │ │ ├── bg_user_search_input.xml
│ │ │ ├── ic_account_clock_24.xml
│ │ │ ├── ic_account_multiple_remove_24.xml
│ │ │ ├── ic_add.xml
│ │ │ ├── ic_archive.xml
│ │ │ ├── ic_arrow_drop_down_24.xml
│ │ │ ├── ic_arrow_upward_24.xml
│ │ │ ├── ic_baseline_check_circle_24.xml
│ │ │ ├── ic_block_24.xml
│ │ │ ├── ic_bookmark.xml
│ │ │ ├── ic_border_style_flipped_24.xml
│ │ │ ├── ic_camera_24.xml
│ │ │ ├── ic_cancel.xml
│ │ │ ├── ic_check_24.xml
│ │ │ ├── ic_check_all_24.xml
│ │ │ ├── ic_checkbox_multiple_blank.xml
│ │ │ ├── ic_checkbox_multiple_blank_stroke.xml
│ │ │ ├── ic_circle_check.xml
│ │ │ ├── ic_class_24.xml
│ │ │ ├── ic_clock_alert_outline_24.xml
│ │ │ ├── ic_close_24.xml
│ │ │ ├── ic_cloud_download_24.xml
│ │ │ ├── ic_dashboard_24.xml
│ │ │ ├── ic_delete.xml
│ │ │ ├── ic_download.xml
│ │ │ ├── ic_download_circle_24.xml
│ │ │ ├── ic_explore_24.xml
│ │ │ ├── ic_face_24.xml
│ │ │ ├── ic_file_24.xml
│ │ │ ├── ic_folder_24.xml
│ │ │ ├── ic_forward_5_24.xml
│ │ │ ├── ic_forward_5_24_a50.xml
│ │ │ ├── ic_forward_5_24_states.xml
│ │ │ ├── ic_highlight_off_24.xml
│ │ │ ├── ic_home_24.xml
│ │ │ ├── ic_image_24.xml
│ │ │ ├── ic_keyboard_24.xml
│ │ │ ├── ic_keyboard_arrow_down_24.xml
│ │ │ ├── ic_keyboard_arrow_up_24.xml
│ │ │ ├── ic_launcher_background.xml
│ │ │ ├── ic_like.xml
│ │ │ ├── ic_logout_24.xml
│ │ │ ├── ic_message_24.xml
│ │ │ ├── ic_more_horiz_24.xml
│ │ │ ├── ic_more_vert_24.xml
│ │ │ ├── ic_not_liked.xml
│ │ │ ├── ic_notes_24.xml
│ │ │ ├── ic_notif.xml
│ │ │ ├── ic_open_in_new_24.xml
│ │ │ ├── ic_outline_class_24.xml
│ │ │ ├── ic_outline_comments_24.xml
│ │ │ ├── ic_outline_info_24.xml
│ │ │ ├── ic_outline_map_24.xml
│ │ │ ├── ic_outline_person_add_24.xml
│ │ │ ├── ic_outline_person_add_disabled_24.xml
│ │ │ ├── ic_outline_person_pin_24.xml
│ │ │ ├── ic_outline_settings_24.xml
│ │ │ ├── ic_outline_star_plus_24.xml
│ │ │ ├── ic_outline_views_24.xml
│ │ │ ├── ic_pause_24.xml
│ │ │ ├── ic_person_24.xml
│ │ │ ├── ic_photo_filter.xml
│ │ │ ├── ic_play_arrow_24.xml
│ │ │ ├── ic_play_arrow_24_a50.xml
│ │ │ ├── ic_play_circle_outline_24.xml
│ │ │ ├── ic_play_states.xml
│ │ │ ├── ic_profile_24.xml
│ │ │ ├── ic_profile_40.xml
│ │ │ ├── ic_profile_48.xml
│ │ │ ├── ic_radio_button_unchecked_24.xml
│ │ │ ├── ic_refresh_24.xml
│ │ │ ├── ic_replay_5_24.xml
│ │ │ ├── ic_replay_5_24_a50.xml
│ │ │ ├── ic_replay_5_24_states.xml
│ │ │ ├── ic_round_add_circle_24.xml
│ │ │ ├── ic_round_arrow_back_24.xml
│ │ │ ├── ic_round_attach_file_rot45_24.xml
│ │ │ ├── ic_round_backspace_24.xml
│ │ │ ├── ic_round_bookmark_border_24.xml
│ │ │ ├── ic_round_check_circle_24.xml
│ │ │ ├── ic_round_crop_24.xml
│ │ │ ├── ic_round_drag_handle_24.xml
│ │ │ ├── ic_round_edit_24.xml
│ │ │ ├── ic_round_emoji_emotions_24.xml
│ │ │ ├── ic_round_emoji_events_24.xml
│ │ │ ├── ic_round_emoji_flags_24.xml
│ │ │ ├── ic_round_emoji_food_beverage_24.xml
│ │ │ ├── ic_round_emoji_nature_24.xml
│ │ │ ├── ic_round_emoji_objects_24.xml
│ │ │ ├── ic_round_emoji_symbols_24.xml
│ │ │ ├── ic_round_emoji_transportation_24.xml
│ │ │ ├── ic_round_flip_camera_24.xml
│ │ │ ├── ic_round_gif_24.xml
│ │ │ ├── ic_round_location_on_24.xml
│ │ │ ├── ic_round_mode_comment_24.xml
│ │ │ ├── ic_round_pause_24.xml
│ │ │ ├── ic_round_play_arrow_24.xml
│ │ │ ├── ic_round_remove_circle_24.xml
│ │ │ ├── ic_round_reply_24.xml
│ │ │ ├── ic_round_send_24.xml
│ │ │ ├── ic_round_tune_24.xml
│ │ │ ├── ic_round_unknown_24.xml
│ │ │ ├── ic_rounded_corner_24.xml
│ │ │ ├── ic_search_24.xml
│ │ │ ├── ic_settings_backup_restore_24.xml
│ │ │ ├── ic_shutter.xml
│ │ │ ├── ic_shutter_focused.xml
│ │ │ ├── ic_shutter_normal.xml
│ │ │ ├── ic_shutter_pressed.xml
│ │ │ ├── ic_slider_24.xml
│ │ │ ├── ic_star_24.xml
│ │ │ ├── ic_star_check_24.xml
│ │ │ ├── ic_sticker_curved_outlines.xml
│ │ │ ├── ic_story_list.xml
│ │ │ ├── ic_story_viewer_list.xml
│ │ │ ├── ic_submit.xml
│ │ │ ├── ic_suggested_users.xml
│ │ │ ├── ic_unread_indicator_24.xml
│ │ │ ├── ic_video_24.xml
│ │ │ ├── ic_view_agenda_24.xml
│ │ │ ├── ic_view_grid_24.xml
│ │ │ ├── ic_volume_off_24.xml
│ │ │ ├── ic_volume_off_24_a50.xml
│ │ │ ├── ic_volume_off_24_states.xml
│ │ │ ├── ic_volume_up_24.xml
│ │ │ ├── ic_volume_up_24_a50.xml
│ │ │ ├── ic_volume_up_24_states.xml
│ │ │ ├── ic_warning.xml
│ │ │ ├── launch.xml
│ │ │ ├── launch_dark.xml
│ │ │ ├── launch_screen.xml
│ │ │ ├── lock.xml
│ │ │ ├── popup_background_exoplayer.xml
│ │ │ ├── pref_list_divider_material.xml
│ │ │ ├── recv_basket_animated.xml
│ │ │ ├── recv_ic_arrow.xml
│ │ │ ├── recv_ic_delete.xml
│ │ │ ├── recv_ic_mic.xml
│ │ │ ├── rounder_corner_bg.xml
│ │ │ ├── rounder_corner_semi_black_bg.xml
│ │ │ ├── shape_oval_light.xml
│ │ │ ├── sl_favourite_24.xml
│ │ │ └── speed_text_color_states.xml
│ │ ├── layout/
│ │ │ ├── activity_camera.xml
│ │ │ ├── activity_crash_error.xml
│ │ │ ├── activity_directory_select.xml
│ │ │ ├── activity_login.xml
│ │ │ ├── activity_main.xml
│ │ │ ├── dialog_account_switcher.xml
│ │ │ ├── dialog_create_backup.xml
│ │ │ ├── dialog_keywords_filter.xml
│ │ │ ├── dialog_opening_post.xml
│ │ │ ├── dialog_post_layout_preferences.xml
│ │ │ ├── dialog_post_view.xml
│ │ │ ├── dialog_profilepic.xml
│ │ │ ├── dialog_restore_backup.xml
│ │ │ ├── dialog_time_settings.xml
│ │ │ ├── fragment_collection_posts.xml
│ │ │ ├── fragment_comments.xml
│ │ │ ├── fragment_direct_messages_inbox.xml
│ │ │ ├── fragment_direct_messages_settings.xml
│ │ │ ├── fragment_direct_messages_thread.xml
│ │ │ ├── fragment_direct_pending_inbox.xml
│ │ │ ├── fragment_discover.xml
│ │ │ ├── fragment_favorites.xml
│ │ │ ├── fragment_feed.xml
│ │ │ ├── fragment_filters.xml
│ │ │ ├── fragment_followers_viewer.xml
│ │ │ ├── fragment_hashtag.xml
│ │ │ ├── fragment_image_edit.xml
│ │ │ ├── fragment_likes.xml
│ │ │ ├── fragment_location.xml
│ │ │ ├── fragment_notifications_viewer.xml
│ │ │ ├── fragment_profile.xml
│ │ │ ├── fragment_saved.xml
│ │ │ ├── fragment_saved_collections.xml
│ │ │ ├── fragment_search.xml
│ │ │ ├── fragment_story_list_viewer.xml
│ │ │ ├── fragment_story_viewer.xml
│ │ │ ├── fragment_topic_posts.xml
│ │ │ ├── fragment_user_search.xml
│ │ │ ├── header_follow.xml
│ │ │ ├── item_comment.xml
│ │ │ ├── item_dir_list.xml
│ │ │ ├── item_discover_topic.xml
│ │ │ ├── item_emoji_grid.xml
│ │ │ ├── item_fav_section_header.xml
│ │ │ ├── item_feed_grid.xml
│ │ │ ├── item_feed_photo.xml
│ │ │ ├── item_feed_slider.xml
│ │ │ ├── item_feed_top.xml
│ │ │ ├── item_feed_video.xml
│ │ │ ├── item_filter.xml
│ │ │ ├── item_follow.xml
│ │ │ ├── item_highlight.xml
│ │ │ ├── item_keyword.xml
│ │ │ ├── item_media.xml
│ │ │ ├── item_notification.xml
│ │ │ ├── item_post.xml
│ │ │ ├── item_pref_divider.xml
│ │ │ ├── item_search_result.xml
│ │ │ ├── item_slider_photo.xml
│ │ │ ├── item_story.xml
│ │ │ ├── item_tab_order_pref.xml
│ │ │ ├── layout_controls.xml
│ │ │ ├── layout_direct_item_options.xml
│ │ │ ├── layout_directory_chooser.xml
│ │ │ ├── layout_dm_action_log.xml
│ │ │ ├── layout_dm_animated_media.xml
│ │ │ ├── layout_dm_base.xml
│ │ │ ├── layout_dm_header.xml
│ │ │ ├── layout_dm_inbox_item.xml
│ │ │ ├── layout_dm_like.xml
│ │ │ ├── layout_dm_link.xml
│ │ │ ├── layout_dm_media.xml
│ │ │ ├── layout_dm_media_share.xml
│ │ │ ├── layout_dm_pending_user_item.xml
│ │ │ ├── layout_dm_profile.xml
│ │ │ ├── layout_dm_raven_media.xml
│ │ │ ├── layout_dm_reel_share.xml
│ │ │ ├── layout_dm_story_share.xml
│ │ │ ├── layout_dm_text.xml
│ │ │ ├── layout_dm_user_item.xml
│ │ │ ├── layout_dm_voice_media.xml
│ │ │ ├── layout_emoji_variant_popup.xml
│ │ │ ├── layout_exo_custom_controls.xml
│ │ │ ├── layout_gif_picker.xml
│ │ │ ├── layout_hashtag_details.xml
│ │ │ ├── layout_include_custom_format_info.xml
│ │ │ ├── layout_include_toolbar.xml
│ │ │ ├── layout_location_details.xml
│ │ │ ├── layout_post_view_bottom.xml
│ │ │ ├── layout_profile_details.xml
│ │ │ ├── layout_searchview.xml
│ │ │ ├── layout_video_player_with_thumbnail.xml
│ │ │ ├── pref_account_switcher.xml
│ │ │ ├── pref_auto_refresh_dm_freq.xml
│ │ │ ├── pref_more_header.xml
│ │ │ └── record_view_layout.xml
│ │ ├── layout-land/
│ │ │ └── activity_camera.xml
│ │ ├── menu/
│ │ │ ├── bottom_nav_menu.xml
│ │ │ ├── collection_posts_menu.xml
│ │ │ ├── comment_options_menu.xml
│ │ │ ├── dm_inbox_menu.xml
│ │ │ ├── dm_thread_menu.xml
│ │ │ ├── feed_menu.xml
│ │ │ ├── follow.xml
│ │ │ ├── hashtag_menu.xml
│ │ │ ├── location_menu.xml
│ │ │ ├── main_menu.xml
│ │ │ ├── menu.xml
│ │ │ ├── multi_select_download_menu.xml
│ │ │ ├── post_view_menu.xml
│ │ │ ├── profile_menu.xml
│ │ │ ├── saved.xml
│ │ │ ├── saved_collection_menu.xml
│ │ │ ├── saved_collection_select_menu.xml
│ │ │ ├── saved_viewer_menu.xml
│ │ │ ├── search.xml
│ │ │ ├── speed_menu.xml
│ │ │ ├── story_menu.xml
│ │ │ └── topic_posts_menu.xml
│ │ ├── mipmap-anydpi-v26/
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ ├── navigation/
│ │ │ ├── direct_messages_nav_graph.xml
│ │ │ ├── discover_nav_graph.xml
│ │ │ ├── favorites_nav_graph.xml
│ │ │ ├── feed_nav_graph.xml
│ │ │ ├── more_nav_graph.xml
│ │ │ ├── notification_viewer_nav_graph.xml
│ │ │ ├── profile_nav_graph.xml
│ │ │ ├── root_nav_graph.xml
│ │ │ └── settings_nav_graph.xml
│ │ ├── raw/
│ │ │ └── emojis.json
│ │ ├── values/
│ │ │ ├── arrays.xml
│ │ │ ├── attrs.xml
│ │ │ ├── bool.xml
│ │ │ ├── color.xml
│ │ │ ├── dimens.xml
│ │ │ ├── drawables.xml
│ │ │ ├── font_certs.xml
│ │ │ ├── ids.xml
│ │ │ ├── strings.xml
│ │ │ ├── styles.xml
│ │ │ └── themes.xml
│ │ ├── values-ar/
│ │ │ ├── arrays.xml
│ │ │ └── strings.xml
│ │ ├── values-ca/
│ │ │ ├── arrays.xml
│ │ │ └── strings.xml
│ │ ├── values-cs/
│ │ │ ├── arrays.xml
│ │ │ └── strings.xml
│ │ ├── values-de/
│ │ │ ├── arrays.xml
│ │ │ └── strings.xml
│ │ ├── values-el/
│ │ │ ├── arrays.xml
│ │ │ └── strings.xml
│ │ ├── values-es/
│ │ │ ├── arrays.xml
│ │ │ └── strings.xml
│ │ ├── values-eu/
│ │ │ ├── arrays.xml
│ │ │ └── strings.xml
│ │ ├── values-fa/
│ │ │ ├── arrays.xml
│ │ │ └── strings.xml
│ │ ├── values-fr/
│ │ │ ├── arrays.xml
│ │ │ └── strings.xml
│ │ ├── values-hi/
│ │ │ ├── arrays.xml
│ │ │ └── strings.xml
│ │ ├── values-in/
│ │ │ ├── arrays.xml
│ │ │ └── strings.xml
│ │ ├── values-it/
│ │ │ ├── arrays.xml
│ │ │ └── strings.xml
│ │ ├── values-ja/
│ │ │ ├── arrays.xml
│ │ │ └── strings.xml
│ │ ├── values-kn/
│ │ │ └── arrays.xml
│ │ ├── values-ko/
│ │ │ ├── arrays.xml
│ │ │ └── strings.xml
│ │ ├── values-land/
│ │ │ └── dimens.xml
│ │ ├── values-mk/
│ │ │ ├── arrays.xml
│ │ │ └── strings.xml
│ │ ├── values-night/
│ │ │ ├── bool.xml
│ │ │ ├── color.xml
│ │ │ └── styles.xml
│ │ ├── values-nl/
│ │ │ ├── arrays.xml
│ │ │ └── strings.xml
│ │ ├── values-or/
│ │ │ ├── arrays.xml
│ │ │ └── strings.xml
│ │ ├── values-pl/
│ │ │ ├── arrays.xml
│ │ │ └── strings.xml
│ │ ├── values-pt/
│ │ │ ├── arrays.xml
│ │ │ └── strings.xml
│ │ ├── values-ru/
│ │ │ ├── arrays.xml
│ │ │ └── strings.xml
│ │ ├── values-sk/
│ │ │ ├── arrays.xml
│ │ │ └── strings.xml
│ │ ├── values-sv/
│ │ │ ├── arrays.xml
│ │ │ └── strings.xml
│ │ ├── values-tr/
│ │ │ ├── arrays.xml
│ │ │ └── strings.xml
│ │ ├── values-vi/
│ │ │ ├── arrays.xml
│ │ │ └── strings.xml
│ │ ├── values-zh-rCN/
│ │ │ ├── arrays.xml
│ │ │ └── strings.xml
│ │ ├── values-zh-rTW/
│ │ │ ├── arrays.xml
│ │ │ └── strings.xml
│ │ └── xml/
│ │ ├── backup_descriptor.xml
│ │ └── provider_paths.xml
│ └── test/
│ └── java/
│ └── awais/
│ └── instagrabber/
│ ├── LiveDataTestUtil.kt
│ ├── MainCoroutineScopeRule.kt
│ ├── common/
│ │ └── Adapters.kt
│ ├── utils/
│ │ ├── CubicInterpolationTest.java
│ │ ├── DownloadUtilsTest.java
│ │ ├── IntentUtilsTest.java
│ │ └── TextUtilsTest.java
│ └── viewmodels/
│ └── ProfileFragmentViewModelTest.kt
├── build.gradle
├── crowdin.yml
├── fastlane/
│ └── metadata/
│ └── android/
│ ├── en-US/
│ │ ├── changelogs/
│ │ │ ├── 32.txt
│ │ │ ├── 33.txt
│ │ │ ├── 36.txt
│ │ │ ├── 37.txt
│ │ │ ├── 38.txt
│ │ │ ├── 39.txt
│ │ │ ├── 40.txt
│ │ │ ├── 41.txt
│ │ │ ├── 42.txt
│ │ │ ├── 43.txt
│ │ │ ├── 44.txt
│ │ │ ├── 45.txt
│ │ │ ├── 46.txt
│ │ │ ├── 47.txt
│ │ │ ├── 48.txt
│ │ │ ├── 49.txt
│ │ │ ├── 52.txt
│ │ │ ├── 53.txt
│ │ │ ├── 54.txt
│ │ │ ├── 55.txt
│ │ │ ├── 56.txt
│ │ │ ├── 57.txt
│ │ │ ├── 60.txt
│ │ │ ├── 61.txt
│ │ │ ├── 62.txt
│ │ │ ├── 63.txt
│ │ │ ├── 64.txt
│ │ │ └── 65.txt
│ │ ├── full_description.txt
│ │ └── short_description.txt
│ └── fr-FR/
│ ├── full_description.txt
│ └── short_description.txt
├── gradle/
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── renovate.json
└── settings.gradle
================================================
FILE CONTENTS
================================================
================================================
FILE: .all-contributorsrc
================================================
{
"files": [
"README.md"
],
"imageSize": 100,
"commit": false,
"badgeTemplate": "[](#contributors)",
"contributors": [
{
"login": "austinhuang0131",
"name": "Austin Huang",
"avatar_url": "https://avatars1.githubusercontent.com/u/16656689",
"profile": "https://austinhuang.me",
"contributions": [
"code",
"doc",
"question",
"translation",
"ideas"
]
},
{
"login": "ammargitham",
"name": "Ammar Githam",
"avatar_url": "https://avatars0.githubusercontent.com/u/8017365",
"profile": "https://github.com/ammargitham",
"contributions": [
"code",
"design",
"ideas",
"maintenance",
"question"
]
},
{
"login": "zerrium",
"name": "Zerrium",
"avatar_url": "https://avatars.githubusercontent.com/u/58355441?v=4",
"profile": "https://github.com/zerrium",
"contributions": [
"code"
]
},
{
"login": "junhuicoding",
"name": "Chua Jun Hui",
"avatar_url": "https://avatars.githubusercontent.com/u/54289027?v=4",
"profile": "https://github.com/junhuicoding",
"contributions": [
"code"
]
},
{
"login": "andersonvom",
"name": "Anderson Mesquita",
"avatar_url": "https://avatars3.githubusercontent.com/u/69922?v=4",
"profile": "https://github.com/andersonvom",
"contributions": [
"code",
"bug"
]
},
{
"login": "MeLlamoPablo",
"name": "Pablo Rodríguez",
"avatar_url": "https://avatars.githubusercontent.com/u/11708035?v=4",
"profile": "https://github.com/MeLlamoPablo",
"contributions": [
"code"
]
},
{
"login": "tcely",
"name": "tcely",
"avatar_url": "https://avatars.githubusercontent.com/u/138864?v=4",
"profile": "https://github.com/tcely",
"contributions": [
"code"
]
},
{
"login": "Vonter",
"name": "Vonter",
"avatar_url": "https://avatars.githubusercontent.com/u/25414711?v=4",
"profile": "https://github.com/Vonter",
"contributions": [
"code"
]
},
{
"login": "raniapl",
"name": "Rania Pilioura",
"avatar_url": "https://avatars.githubusercontent.com/u/56370534?v=4",
"profile": "https://github.com/raniapl",
"contributions": [
"code"
]
},
{
"login": "stamatiap",
"name": "Stamatia Papageorgiou",
"avatar_url": "https://avatars.githubusercontent.com/u/57223967?v=4",
"profile": "https://github.com/stamatiap",
"contributions": [
"code",
"translation"
]
},
{
"login": "Zopieux",
"name": "Alexandre Macabies",
"avatar_url": "https://avatars.githubusercontent.com/u/81353?v=4",
"profile": "https://github.com/Zopieux",
"contributions": [
"code"
]
},
{
"login": "vojta-horanek",
"name": "Vojtěch Hořánek",
"avatar_url": "https://avatars.githubusercontent.com/u/12630566?v=4",
"profile": "https://vojtechh.eu/",
"contributions": [
"code",
"translation"
]
},
{
"login": "The-EDev",
"name": "Farook Al-Sammarraie",
"avatar_url": "https://avatars.githubusercontent.com/u/60552923?v=4",
"profile": "https://github.com/The-EDev",
"contributions": [
"code"
]
},
{
"login": "snajdovski",
"name": "Stefan Najdovski",
"avatar_url": "https://avatars2.githubusercontent.com/u/42580385?v=4",
"profile": "https://snajdovski.github.io",
"contributions": [
"design",
"translation"
]
},
{
"login": "CrazyMarvin",
"name": "CrazyMarvin",
"avatar_url": "https://avatars3.githubusercontent.com/u/15004217?v=4",
"profile": "https://github.com/CrazyMarvin",
"contributions": [
"financial"
]
},
{
"login": "KevinNThomas",
"name": "Kevin Thomas",
"avatar_url": "https://avatars2.githubusercontent.com/u/15370181",
"profile": "http://kevinthomas.dev",
"contributions": [
"financial"
]
},
{
"login": "Martin5001",
"name": "Martin Krejčí",
"avatar_url": "https://avatars.githubusercontent.com/u/35201200?v=4",
"profile": "https://github.com/Martin5001",
"contributions": [
"bug",
"ideas",
"translation"
]
},
{
"login": "Shadowspear123",
"name": "Shadowspear123",
"avatar_url": "https://avatars1.githubusercontent.com/u/50462281",
"profile": "https://github.com/Shadowspear123",
"contributions": [
"blog",
"bug",
"ideas",
"question"
]
},
{
"login": "RickyM7",
"name": "Ricardo",
"avatar_url": "https://avatars3.githubusercontent.com/u/24703825?v=4",
"profile": "https://github.com/RickyM7",
"contributions": [
"bug",
"translation"
]
},
{
"login": "Akrai",
"name": "Akrai",
"avatar_url": "https://avatars1.githubusercontent.com/u/5624597?v=4",
"profile": "https://github.com/Akrai",
"contributions": [
"ideas",
"translation"
]
},
{
"login": "avtkal",
"name": "avtkal",
"avatar_url": "https://avatars.githubusercontent.com/u/63205014?v=4",
"profile": "https://github.com/avtkal",
"contributions": [
"translation"
]
},
{
"login": "cizordj",
"name": "Cézar Augusto",
"avatar_url": "https://avatars2.githubusercontent.com/u/32869222?v=4",
"profile": "https://github.com/cizordj",
"contributions": [
"translation"
]
},
{
"login": "dimitrist19",
"name": "Dimitris T",
"avatar_url": "https://avatars.githubusercontent.com/u/56406468?v=4",
"profile": "https://github.com/dimitrist19",
"contributions": [
"translation"
]
},
{
"login": "farzadx",
"name": "farzadx",
"avatar_url": "https://avatars2.githubusercontent.com/u/70059397?v=4",
"profile": "https://github.com/farzadx",
"contributions": [
"translation"
]
},
{
"login": "faydin",
"name": "Fatih Aydın",
"avatar_url": "https://avatars2.githubusercontent.com/u/22706676?v=4",
"profile": "https://github.com/faydin",
"contributions": [
"translation"
]
},
{
"login": "fouze555",
"name": "fouze555",
"avatar_url": "https://avatars3.githubusercontent.com/u/71935341?v=4",
"profile": "https://github.com/fouze555",
"contributions": [
"translation"
]
},
{
"login": "Galang23",
"name": "Galang23",
"avatar_url": "https://avatars3.githubusercontent.com/u/13700948",
"profile": "https://github.com/Galang23",
"contributions": [
"translation"
]
},
{
"login": "initdebugs",
"name": "Initdebugs",
"avatar_url": "https://avatars0.githubusercontent.com/u/75781464?v=4",
"profile": "https://github.com/initdebugs",
"contributions": [
"translation"
]
},
{
"login": "CrafterSvK",
"name": "Jakub Janek",
"avatar_url": "https://avatars3.githubusercontent.com/u/8365659?v=4",
"profile": "https://janek.xyz/",
"contributions": [
"translation"
]
},
{
"login": "GenosseFlosse",
"name": "GenosseFlosse",
"avatar_url": "https://avatars.githubusercontent.com/u/59205524?v=4",
"profile": "https://github.com/GenosseFlosse",
"contributions": [
"translation"
]
},
{
"login": "kernoeb",
"name": "kernoeb",
"avatar_url": "https://avatars3.githubusercontent.com/u/24623168",
"profile": "https://becauseofprog.fr/",
"contributions": [
"translation"
]
},
{
"login": "Lego8486",
"name": "Ten_Lego",
"avatar_url": "https://avatars1.githubusercontent.com/u/47414485",
"profile": "https://github.com/Lego8486",
"contributions": [
"translation"
]
},
{
"login": "MoaufmKlo",
"name": "MoaufmKlo",
"avatar_url": "https://avatars1.githubusercontent.com/u/45636897",
"profile": "https://github.com/MoaufmKlo",
"contributions": [
"translation"
]
},
{
"login": "nalinalini",
"name": "nalinalini",
"avatar_url": "https://avatars0.githubusercontent.com/u/65640431?v=4",
"profile": "https://github.com/nalinalini",
"contributions": [
"translation"
]
},
{
"login": "peterge1998",
"name": "peterge1998",
"avatar_url": "https://avatars2.githubusercontent.com/u/47355238",
"profile": "https://github.com/peterge1998",
"contributions": [
"translation"
]
},
{
"login": "PierreM0",
"name": "PierreM0",
"avatar_url": "https://avatars3.githubusercontent.com/u/71077853?v=4",
"profile": "https://github.com/PierreM0",
"contributions": [
"translation"
]
},
{
"login": "Pyrobauve",
"name": "Pyrobauve",
"avatar_url": "https://avatars.githubusercontent.com/u/48654473?v=4",
"profile": "https://github.com/Pyrobauve",
"contributions": [
"translation"
]
},
{
"login": "RAMAR-RAR",
"name": "RAMAR-RAR",
"avatar_url": "https://avatars3.githubusercontent.com/u/47423745",
"profile": "https://github.com/RAMAR-RAR",
"contributions": [
"translation"
]
},
{
"login": "rohang02",
"name": "rohang02",
"avatar_url": "https://avatars3.githubusercontent.com/u/47921164?v=4",
"profile": "https://github.com/rohang02",
"contributions": [
"translation"
]
},
{
"login": "retiolus",
"name": "retiolus",
"avatar_url": "https://avatars1.githubusercontent.com/u/65604466?v=4",
"profile": "https://github.com/retiolus",
"contributions": [
"translation"
]
},
{
"login": "rex07",
"name": "Rex_sa",
"avatar_url": "https://avatars.githubusercontent.com/u/13156001?v=4",
"profile": "https://github.com/rex07",
"contributions": [
"translation"
]
},
{
"login": "rikishi0071",
"name": "rikishi0071",
"avatar_url": "https://avatars3.githubusercontent.com/u/18183855?v=4",
"profile": "https://github.com/rikishi0071",
"contributions": [
"translation"
]
},
{
"login": "sandboiii",
"name": "Alexey Peschany",
"avatar_url": "https://avatars.githubusercontent.com/u/17468894?v=4",
"profile": "https://gitlab.com/sandboiii",
"contributions": [
"translation"
]
},
{
"login": "Sitavi",
"name": "Sitavi",
"avatar_url": "https://avatars.githubusercontent.com/u/80586127?v=4",
"profile": "https://github.com/Sitavi",
"contributions": [
"translation"
]
},
{
"login": "Still34",
"name": "Still Hsu",
"avatar_url": "https://avatars2.githubusercontent.com/u/5843208?v=4",
"profile": "https://stillu.cc/",
"contributions": [
"translation"
]
},
{
"login": "Umeaboy",
"name": "Kristoffer Grundström",
"avatar_url": "https://avatars.githubusercontent.com/u/714473?v=4",
"profile": "https://github.com/Umeaboy",
"contributions": [
"translation"
]
},
{
"login": "wagnim",
"name": "wagnim",
"avatar_url": "https://avatars0.githubusercontent.com/u/30241419",
"profile": "https://github.com/wagnim",
"contributions": [
"translation"
]
},
{
"login": "wokija",
"name": "wokija",
"avatar_url": "https://avatars.githubusercontent.com/u/14982166?v=4",
"profile": "https://github.com/wokija",
"contributions": [
"translation"
]
},
{
"login": "ysakamoto",
"name": "ysakamoto",
"avatar_url": "https://avatars3.githubusercontent.com/u/1331642?v=4",
"profile": "https://github.com/ysakamoto",
"contributions": [
"translation"
]
},
{
"login": "ZDVokoun",
"name": "ZDVokoun",
"avatar_url": "https://avatars.githubusercontent.com/u/76393152?v=4",
"profile": "https://github.com/ZDVokoun",
"contributions": [
"translation"
]
},
{
"login": "2hot2exist",
"name": "2hot2exist",
"avatar_url": "https://avatars.githubusercontent.com/u/84233003?v=4",
"profile": "https://github.com/2hot2exist",
"contributions": [
"translation"
]
}
],
"contributorsPerLine": 6,
"projectName": "barinsta",
"projectOwner": "austinhuang0131",
"repoType": "github",
"repoHost": "https://github.com",
"skipCi": true,
"types": {
"translation": {
"symbol": "🌍",
"description": "Translation",
"link": "https://crowdin.com/project/instagrabber"
}
},
"commitConvention": "none"
}
================================================
FILE: .codebeatsettings
================================================
{
"JAVA": {
"TOO_MANY_IVARS": [8, 10, 20, 30]
}
}
================================================
FILE: .github/CODE_OF_CONDUCT.md
================================================
# Contributor Code of Conduct
This project adheres to No Code of Conduct. We are all adults. We accept anyone's contributions. Nothing else matters.
For more information please visit the [No Code of Conduct](https://github.com/domgetter/NCoC) homepage.
================================================
FILE: .github/CONTRIBUTING.md
================================================
## WARNING
* All forks must respect [GPLv3](https://www.gnu.org/licenses/gpl-3.0.html). Please report violations in Issues or [confidentially](https://austinhuang.me/#hey-you-look-cool).
* Some people have asked me about publishing a "commercial" fork that serves ads. However, if you do properly comply with GPLv3, users would realize that the original non-commercial version exists and, in turn, abandon your fork. And if you don't comply, you get copystriked.
* Although publishing a fork is allowed by license, it is strongly discouraged to do so as it divides the effort and creates confusion (as well as for the reason above). It is, therefore, recommended to send a pull request back to us, so that the larger community can enjoy your improvement. (This does not apply if you're adapting this app for a different platform other than Instagram.)
## Contributing
Thank you for your interest in Barinsta!
Our vision is an open source true alternative of the official Instagram app. It is Austin's pursuit of a libre life that lead him to this app during its abandonment, and it was one unresolved bug that made him have the enthusiasm in implementing numerous features for this app, despite having 0 knowledge of Java beforehand.
As we grow in popularity, it becomes apparent that we are short on hands. Every contribution counts!
## I want to help coding it!
Great!
Generally, we want to imitate features in the actual Instagram app. There are many Instagram private API repositories on GitHub for you to refer to. Note that you should minimize POST: If a job should be done with GET, then there has to be a GET endpoint. (Indeed, sometimes you need multiple repositories for reference.)
As long as you have tested your version (please indicate device and API version) and make sure it works, then you can submit a PR! Large UI changes have to be voted on by the community, so it would be helpful to upload some screenshots.
Check errors are for reference only. Try to minimize them, but usually they don't make a big difference.
**NEVER touch the l10n-master branch.** It's automatically managed by Crowdin.
The legacy branch is no longer maintained.
### I can't code Java, but I want to!
Fun fact: Austin took over this project and learned Java on the fly (I'm not joking, I only do JavaScript before taking this over).
Even though Java is quite annoying, it is still possible to learn it by trying to understand what these code do (Easier if you have coding experience in other languages).
If you have questions, don't be afraid to ask for help from any current maintainer!
## I found a bug!
**Please read [FAQ](https://barinsta.austinhuang.me/en/latest/faq.html) first.**
Bugs are inevitable during active development, as nobody can cover all the possible test cases.
You can either email your crash dump to `barinsta@austinhuang.me` (The crash reporter will fill in this address for you) or create a GitHub issue. If you're on GitHub, please follow the template. If you're reporting by email, your email address will be published in the GitHub issue. You can contact me [privately](https://austinhuang.me/#hey-you-look-cool) or [through support channels](https://barinsta.austinhuang.me/en/latest/#contact-us) to remove it.
Generally, reporting bugs directly in support channels is not recommended, as they can be difficult to find.
### I want to help... in some other way.
You can...
* translate it [](https://crowdin.com/project/instagrabber)
* promote it (reddit [r/AndroidApps](https://www.reddit.com/r/androidapps/comments/i30tpp/instagrabber_an_open_source_instagram_client/), YouTube [HowToMen](https://www.youtube.com/watch?v=1Um2ZJG_mB4), [Ekşi Sözlük](https://eksisozluk.com/instagrabber--6643143))
* star it [](https://github.com/austinhuang0131/barinsta/stargazers)
Happy contributing!
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: austinhuang0131
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: austinhuang
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: austinhuang
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: ["https://austinhuang.me/donate"]
================================================
FILE: .github/ISSUE_TEMPLATE/ban_report.md
================================================
---
name: Ban report
about: If you suspect a ban by Instagram due to your usage of this app, you MUST report it.
title: "[BAN]"
labels: BAN REPORT
assignees: 'austinhuang0131'
---
## Answer honestly. All boxes must be checked for this report to be considered.
- [ ] My app is on the latest version available on GitHub or F-Droid. I understand that any other version is not supported.
- [ ] I certify that all actions I have performed on the app constitute human behaviour. I am using the app responsibly and in a way identical to using the official app. Specifically, I did not perform botting or automated key clicks.
- [ ] I have considered other possible reasons for my ban, and I cannot find any substantial claim other than the usage of this app.
- [ ] Instagram has indicated that this is a permanent ban, not a required password change or a temporary lock.
## Answer honestly. Check accordingly to your situation.
- [ ] I had prior rule violations on Instagram, specifically:
- [ ] I have admitted the use of Barinsta on Instagram.
- [ ] I have admitted the use of Barinsta to a friend who uses Instagram.
- [ ] I have modified the source code of the app that I use, other than what is present in this repo. Specifically:
## Describe your case, including your usage pattern, but without private information.
## Provide your communication with Instagram, without private information of you.
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: App crashing? You seeing a confusing error message? Report them here!
title: "[BUG]"
labels: bug
assignees: ''
---
- [ ] My app is *at least* at the current release version. I understand that any versions before that is not supported.
- [ ] I have read [the FAQ](https://barinsta.austinhuang.me/en/latest/faq.html).
## Steps
## Environment
- Device:
- Android version:
## Additional info
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: true
contact_links:
- name: Community Chatrooms
url: https://barinsta.austinhuang.me/en/latest/chat.html
about: Chat with developers and users alike!
- name: /r/Barinsta
url: https://reddit.com/r/barinsta
about: Start a discussion on our subreddit!
- name: Repository Discussions
url: https://github.com/austinhuang0131/barinsta/discussions
about: Start a discussion in this repo!
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Want new features? Ask them here!
title: "[FTR]"
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Additional context**
Add any other context or screenshots about the feature request here.
================================================
FILE: .github/issue_label_bot.yaml
================================================
label-alias:
bug: 'bug'
feature_request: 'enhancement'
question: 'question'
================================================
FILE: .github/workflows/github_nightly_release.yml
================================================
name: Github nightly
on:
schedule:
# * is a special character in YAML so you have to quote this string
- cron: '27 10 * * *' # Everyday at 10:27:00
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: set up JDK 1.8
uses: actions/setup-java@v2
with:
distribution: 'zulu'
java-version: '8'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build Github unsigned apk
run: ./gradlew assembleGithubRelease --stacktrace --project-prop pre --project-prop split
- name: Sign APK
uses: ammargitham/sign-android-release@v1.1.1
# ID used to access action output
id: sign_app
with:
releaseDirectory: app/build/outputs/apk/github/release
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
alias: ${{ secrets.ALIAS }}
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
- name: Get current date and time
id: date
run: echo "::set-output name=date::$(date +'%Y%m%d_%H%M%S')"
# Create artifact
- name: Create apk artifact
uses: actions/upload-artifact@v2
with:
name: barinsta_nightly_${{ steps.date.outputs.date }}
# path: ${{steps.sign_app.outputs.signedReleaseFile}}
path: app/build/outputs/apk/github/release/*-signed.apk
# Send success notification
- name: Send success Telegram notification
if: ${{ success() }}
uses: appleboy/telegram-action@master
with:
to: ${{ secrets.TELEGRAM_BUILDS_CHANNEL_TO }}
token: ${{ secrets.TELEGRAM_BUILDS_BOT_TOKEN }}
message: "${{ github.workflow }} ${{ github.job }} #${{ github.run_number }} completed successfully.\nhttps://github.com/${{github.repository}}/actions/runs/${{github.run_id}}"
# document: ${{steps.sign_app.outputs.signedReleaseFile}}
document: app/build/outputs/apk/github/release/*-signed.apk
# Send failure notification
- name: Send failure Telegram notification
if: ${{ failure() }}
uses: appleboy/telegram-action@master
with:
to: ${{ secrets.TELEGRAM_BUILDS_CHANNEL_TO }}
token: ${{ secrets.TELEGRAM_BUILDS_BOT_TOKEN }}
message: "${{ github.workflow }} ${{ github.job }} #${{ github.run_number }} failed.\nhttps://github.com/${{github.repository}}/actions/runs/${{github.run_id}}"
================================================
FILE: .github/workflows/github_pre_release.yml
================================================
name: Github pre-release
on: workflow_dispatch
# push:
# branches: [ master ]
# pull_request:
# branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: set up JDK 1.8
uses: actions/setup-java@v2
with:
distribution: 'zulu'
java-version: '8'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build Github unsigned pre-release apk
run: ./gradlew assembleGithubRelease --stacktrace --project-prop pre --project-prop split
- name: Sign APK
uses: ammargitham/sign-android-release@v1.1.1
# ID used to access action output
id: sign_app
with:
releaseDirectory: app/build/outputs/apk/github/release
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
alias: ${{ secrets.ALIAS }}
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
- name: Get current date and time
id: date
run: echo "::set-output name=date::$(date +'%Y%m%d_%H%M%S')"
# Create artifact
- name: Create apk artifact
uses: actions/upload-artifact@v2
with:
name: barinsta_pre-release_${{ steps.date.outputs.date }}
# path: ${{steps.sign_app.outputs.signedReleaseFile}}
path: app/build/outputs/apk/github/release/*-signed.apk
# Send success notification
- name: Send success Telegram notification
if: ${{ success() }}
uses: appleboy/telegram-action@master
with:
to: ${{ secrets.TELEGRAM_BUILDS_CHANNEL_TO }}
token: ${{ secrets.TELEGRAM_BUILDS_BOT_TOKEN }}
message: "${{ github.workflow }} ${{ github.job }} #${{ github.run_number }} completed successfully.\nURL: https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}"
# document: ${{steps.sign_app.outputs.signedReleaseFile}}
document: app/build/outputs/apk/github/release/*-signed.apk
# Send failure notification
- name: Send failure Telegram notification
if: ${{ failure() }}
uses: appleboy/telegram-action@master
with:
to: ${{ secrets.TELEGRAM_BUILDS_CHANNEL_TO }}
token: ${{ secrets.TELEGRAM_BUILDS_BOT_TOKEN }}
message: "${{ github.workflow }} ${{ github.job }} #${{ github.run_number }} failed.\nURL: https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}"
================================================
FILE: .github/workflows/label-bugs.yml
================================================
name: Label bugs
on:
issues:
types: [opened]
jobs:
add-labels:
runs-on: ubuntu-latest
if: contains(github.event.issue.body, 'New Trace collected:') == true
steps:
- name: Add labels
uses: actions-cool/issues-helper@v2.2.1
with:
actions: 'add-labels'
token: ${{ secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.issue.number }}
labels: 'bug'
- name: Remove runs
uses: GongT/cancel-previous-workflows@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DELETE: 'yes'
================================================
FILE: .github/workflows/label-duplicates.yml
================================================
name: Label duplicates
on:
issue_comment:
types: [created]
jobs:
add-labels:
runs-on: ubuntu-latest
if: contains(github.event.comment.body, 'Duplicate of') == true
steps:
- name: Add labels
uses: actions-cool/issues-helper@v2.2.1
with:
actions: 'add-labels'
token: ${{ secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.issue.number }}
labels: 'duplicate'
- name: Remove runs
uses: GongT/cancel-previous-workflows@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DELETE: 'yes'
================================================
FILE: .gitignore
================================================
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/markdown-navigator.xml
/.idea/markdown-navigator-enh.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
/.idea/git_toolbox_prj.xml
/.idea/dbnavigator.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
app/release
/sentry.properties
/app/fdroid/
/app/github/
/repo
/.fdroid.yml
================================================
FILE: .idea/.gitignore
================================================
# Default ignored files
/shelf/
/dictionaries/
================================================
FILE: .idea/.name
================================================
Barinsta
================================================
FILE: .idea/codeStyles
================================================
.*:id
http://schemas.android.com/apk/res/android
.*:name
http://schemas.android.com/apk/res/android
.*
http://schemas.android.com/apk/res/android
ANDROID_ATTRIBUTE_ORDER
================================================
FILE: .idea/compiler.xml
================================================
================================================
FILE: .idea/gradle.xml
================================================
================================================
FILE: .idea/inspectionProfiles/profiles_settings.xml
================================================
================================================
FILE: .idea/jarRepositories.xml
================================================
================================================
FILE: .idea/misc.xml
================================================
================================================
FILE: .idea/render.experimental.xml
================================================
================================================
FILE: .idea/runConfigurations/app.xml
================================================
================================================
FILE: .idea/runConfigurations.xml
================================================
================================================
FILE: .idea/vcs.xml
================================================
================================================
FILE: .project
================================================
Barinsta
Project instagrabber created by Buildship.
org.eclipse.buildship.core.gradleprojectbuilder
org.eclipse.buildship.core.gradleprojectnature
0
30
org.eclipse.core.resources.regexFilterMatcher
node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__
================================================
FILE: .settings/org.eclipse.buildship.core.prefs
================================================
arguments=
auto.sync=false
build.scans.enabled=false
connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
connection.project.dir=
eclipse.preferences.version=1
gradle.user.home=
java.home=/Library/Java/JavaVirtualMachines/jdk-12.0.1.jdk/Contents/Home
jvm.arguments=
offline.mode=false
override.workspace.settings=true
show.console.view=true
show.executions.view=true
================================================
FILE: CHANGELOG
================================================
v15.9 to v16.5: https://gitlab.com/AwaisKing/instagrabber/-/releases
From v16.6: https://github.com/austinhuang0131/barinsta/releases
v15.9
note: there will be no F-Droid updates from this version (v15.9) and onward, download updates from repo's Releases page.
+ added user stories in Feed
+ added frame to currently showing slider item
+ removed tap to pause/resume from Post viewer, replaced with controller
+ fixed swipe not working when posts are opened from stories
+ fixed comments not showing for slider items
v15.8
+ added user's website in profile
+ fixed caption mentions length (@kernoeb)
+ fixed some translations (@kernoeb)
+ fixd feed captions merging with "... more"
v15.4
+ ADDED FRENCH AND SPANISH!!!
+-> Huge thanks to @kernoeb (Telegram) for French translation and @sguinetti (GitLab) for Spanish translation!!
+ added custom post time format support!
+ fixed flickering after changing settings
+ fixed posts not showing after searching from a private profile
+ fixed stories and profile pictures not downloading in user folders even when option was enabled
+ fixed issues with feed, discover and post viewer
+ fixed search suggestions crashes
v15.2
+ fixed feed video not pausing when opened in post viewer
+ added 1 new profile picture view mode
+ added fields in LogCollector to better understand how things went wrong
+ better comments, story, feed, discover and suggestion fetchers
v15.0
+ added support for Instagram.com urls! (:
+ added "Downloaded" check on posts if they've already been downloaded (as per suggestion)
+ fixed highlights scrolling issues
+ fixed comments issues
+ fixed posts weren't showing after searching anything from a private account
+ fixed suggestions not showing sometimes
+ fixed Stories viewer swipe issues
+ fixed Import/Export dialog not showing on old phones
+ added user's name in suggestions search list
+ added comment likes in Comments viewer
+ sending logs won't add empty logs to archive anymore!
+ fixed no new line in logs
+ handled Login WebView lifecycles
+ a better way of handling highlight swipes
+ removed useless code parts
v14.5
+ added changelog after update
+ added swipe support in both Discover/Explore and Feed pages
+ added Send Logs button in Settings to send logs when something doesn't work
+ added clickable user profile in Post Viewer
+ fixed weirdly collapsing toolbar when toolbar is shown at the top
v14.0
+ added theme selection support
+ added import/export settings, favorites and logins functionality (thanks to Airikr [@edgren] on Telegram)
+ added support for downloading slider items from User Feed page
+ added support for usernames and hashtags in user's biography/about text
+ added multiple selection in Discover/Explore page
+ added post date for feed items
+ added some more date formats
+ copyable feed item caption (long tap)
+ fixed late refresh indicator in Followers/Following comparison mode
+ changed feed item size to squares
+ fixed some caption text issues having mentions and hashtags
+ removed clipboard listener (stopped working after some changes)
+ added log collector for different crash scenarios
v13.7
+ fixed custom download folder selection issues
v13.3
+ added discover/explore page (only for logged in users)
+ added function to remove IPTC tracking data from downloaded pictures (thanks to Airikr [@edgren] on Telegram)
+ added multiple accounts support (quick access) and favorites (a suggestion from Saurabh on Telegram)
+ added custom download folder option, you can select where to download posts (a suggestion from Airikr)
+ added desktop mode toggle in Login activity (a suggestion from Eymen on Telegram)
+ added post date in post viewer (a suggestion from W on Telegram)
+ added post time format settings [ Settings > Post Time Settings ]
+ fixed some icons and layouts
+ removed color from slider items in feed
+ removed useless methods and properties
+ better way of handling multiple stories and posts (sliders) in post viewer
+ tried to make notifications grouped together.. hope they work
+ some other fixes and additions which i probably forgot, cause i'm a human ffs
v13.0
+ fixed crash when searching hashtags
+ added lazy loading for hashtags
+ added Show Feed option in Settings (feed may crash app on some phones)
+ added null check in Download async
+ better/adapatable icon
+ fixed sheet dialog themes
v12.7
+ (probably) fixed inflating issue in some devices
v12.5
+ some small performance improvements
v12.0
+ added feed!! (only for logged in users)
+ fixed multiple hashtags with no spaces between them
+ stopped activity from recreating when nothing changed
+ changed highlights to RecyclerView instead of HorizontalScrollView
+ changed some numbers and precisions cause she left me on read
v11.0
+ added crash reporting library
+ added mute/unmute for session
+ better profile picture viewer + profile picture info
+ better swipe gesture
+ fixed mention and hashtag issues
v10.0
NOTE: YOU MAY NEED TO LOGIN TO VIEW PROFILES CAUSE OF INSTAGRAM CHANGES.
+ added direct download multiple posts dialog
+ fixed notification problems
+ fixed some direct download problems
+ fixed batch download and username folder not creating in some activities
+ fixed update checker
v9.0
+ added search in comments viewer
+ added settings to auto or lazy load posts
+ added user & hashtag stack when back pressed
+ added loading icon when loading posts
+ profile info bar sticks to top
+ fixed highlights and profile picture size in portrait and landscape mode
+ fixed posts loading from other user when restarting app
+ fixed posts size changing when scrolled
+ users & hashtag search shows only users or hashtag when first char is @ or #
+ scrolls to top when back pressed
v8.0
+ added pull-to-refresh layout in main posts, followers/following viewer
+ added search in followers/following viewer
+ added video views in post viewer
+ added animation when showing highlights (if available)
+ fixed long usernames not animating (marquee)
+ fixed padding and size in portrait and landscape modes
+ fixed accounts showing personal followers/following when account has 0 followers/following
+ fixed double ripple when tapped on profile picture
+ smaller story border around profile picture
v7.0
+ added comments viewer!!
+ added highest quality post fetcher
+ added loading indicator where it was missing before
+ fixed highlight name alignment
+ fixed swiping on posts opened via Share or link
v6.0
+ added story highlights!! (issue #5)
+ added button to view posts posted in stories
+ added verified badge for accounts
+ fixed posts & story swiping issues
+ fixed slow loading and stuff
+ fixed different margins and sizes
+ fixed activity not recreating after settings dialogs closed
v5.0
+ added followers / following checker
+ added search view suggestions for usernames & hashtags
+ added sliding profile container
+ fixed batch download permission issue (issue #4)
+ fixed some small screen panning issues
+ fixed search view width
+ fixed update checker
v4.0
+ fixed Login and Visit project page button codes.
v3.0
+ fixed posts merged from different accounts when searched while posts are loading!
+ view stories (only if you're logged in)
+ directly download posts
+ choose between two pfp (profile picture) viewer methods
+ fixed search box not showing up when toolbar is at bottom
+ automatically checks for updates
v2.0
+ fixed Login crashes
v1.0
+ first ever changelog
+ basic stuff like downloading profile pics, posts and copying captions and bio.
+ batch download posts
================================================
FILE: LICENSE
================================================
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
================================================
FILE: README.md
================================================
### THERE ARE CURRENTLY NO OFFICIAL GOOGLE PLAY RELEASES. PLEASE REPORT ANY OCCURRENCES TO US.
# Barinsta
[](https://github.com/humanetech-community/awesome-humane-tech)
[](https://github.com/ellerbrock/open-source-badges/)
[](https://makeapullrequest.com)
[](./LICENSE)
[](https://GitHub.com/austinhuang0131/barinsta/stargazers/)
[](#contributors)
Instagram client; previously known as InstaGrabber.
For documentation, visit [Barinsta.AustinHuang.me](https://barinsta.austinhuang.me).
## Download
**By installing, you indicate your acceptance of [Terms of Service](https://barinsta.austinhuang.me/en/latest/tos.html) and [Privacy Policy](https://barinsta.austinhuang.me/en/latest/privacy.html).**
Version status:  vs. 
## Screenshots
## We need maintainers!
To speed up development, we need more hands on deck. If you are proficient in Java and Android development, and are willing to perform such a public service, please [contact us](https://t.me/austinhuang).
## Contact us
* Use [GitHub issues](https://github.com/austinhuang0131/instagrabber/issues) when possible.
* Email: [Barinsta@AustinHuang.me](mailto:barinsta@austinhuang.me?body=Please%20note%20that%20your%20email%20address%20and%20the%20entire%20content%20will%20be%20published%20onto%20GitHub%20issues.%20If%20you%20do%20not%20wish%20to%20do%20that%2C%20use%20other%20contact%20methods%20instead.) (Synced to GitHub issues)
* Reddit: [](https://reddit.com/r/barinsta)
* Chat (Bridged to each other): [](https://matrix.to/#/#barinsta:matrix.org) [](https://t.me/barinsta_app) [](https://discord.gg/YtEDzN2)
## Contributors
Prominent contributors are listed here in the [all-contributors](https://allcontributors.org/) specifications, see [emoji key](https://allcontributors.org/docs/en/emoji-key). [Want to contribute to Barinsta?](https://github.com/austinhuang0131/barinsta/blob/master/.github/CONTRIBUTING.md)
## License
This app's predecessor, InstaGrabber, was originally made by [@AwaisKing](https://github.com/AwaisKing) on [GitLab](https://gitlab.com/AwaisKing/instagrabber).
Barinsta
Copyright (C) 2020-2021 Austin Huang
Ammar Githam
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
Logo by [Stefan Najdovski](https://snajdovski.github.io/). Used under license.
[](https://snyk.io/test/github/austinhuang0131/barinsta)
[](https://lgtm.com/projects/g/austinhuang0131/barinsta)
[](https://lgtm.com/projects/g/austinhuang0131/barinsta)
[](https://www.codefactor.io/repository/github/austinhuang0131/barinsta)
[](https://www.codacy.com/manual/austinhuang0131/instagrabber)
[](https://crowdin.com/project/instagrabber)
[](https://forthebadge.com)[](https://forthebadge.com) [](https://www.gnu.org/licenses/gpl-3.0.html)
================================================
FILE: SECURITY.md
================================================
If there is a security issue with the latest version, please let me know using GitHub issues (bug), or email `im [at] austinhuang [dot] me` if confidential. Use [this PGP key](https://github.com/austinhuang0131/austinhuang0131.github.io/blob/master/assets/key.asc) if you know how to.
================================================
FILE: app/.classpath
================================================
================================================
FILE: app/.gitignore
================================================
/build
================================================
FILE: app/.project
================================================
app
Project app created by Buildship.
org.eclipse.jdt.core.javabuilder
org.eclipse.buildship.core.gradleprojectbuilder
org.eclipse.jdt.core.javanature
org.eclipse.buildship.core.gradleprojectnature
1600117114944
30
org.eclipse.core.resources.regexFilterMatcher
node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__
================================================
FILE: app/.settings/org.eclipse.buildship.core.prefs
================================================
connection.project.dir=..
eclipse.preferences.version=1
================================================
FILE: app/build.gradle
================================================
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: "androidx.navigation.safeargs"
apply plugin: 'kotlin-kapt'
apply from: 'sentry.gradle'
def getGitHash = { ->
def stdout = new ByteArrayOutputStream()
exec {
commandLine 'git', 'rev-parse', '--short', 'HEAD'
standardOutput = stdout
}
return stdout.toString().trim()
}
android {
compileSdkVersion 30
defaultConfig {
applicationId 'me.austinhuang.instagrabber'
minSdkVersion 21
targetSdkVersion 30
versionCode 65
versionName '19.2.4'
multiDexEnabled true
vectorDrawables.useSupportLibrary = true
vectorDrawables.generatedDensities = []
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
compileOptions {
// Flag to enable support for the new language APIs
coreLibraryDesugaringEnabled true
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_1_8
}
buildFeatures { viewBinding true }
aaptOptions { additionalParameters '--no-version-vectors' }
buildTypes {
debug {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
flavorDimensions "repo"
productFlavors {
github {
dimension "repo"
// versionNameSuffix "-github" // appended in assemble task
buildConfigField("String", "dsn", SENTRY_DSN)
buildConfigField("boolean", "isPre", "false")
}
fdroid {
dimension "repo"
versionNameSuffix "-fdroid"
buildConfigField("boolean", "isPre", "false")
}
}
splits {
// Configures multiple APKs based on ABI.
abi {
// Enables building multiple APKs per ABI.
enable project.hasProperty("split") && !gradle.startParameter.taskNames.isEmpty() && gradle.startParameter.taskNames.get(0).contains('Release')
// By default all ABIs are included, so use reset() and include to specify that we only
// want APKs for x86 and x86_64.
// Resets the list of ABIs that Gradle should create APKs for to none.
reset()
// Specifies a list of ABIs that Gradle should create APKs for.
include "x86", "x86_64", "arm64-v8a", "armeabi-v7a"
// Specifies that we want to also generate a universal APK that includes all ABIs.
universalApk true
}
}
android.applicationVariants.all { variant ->
if (variant.flavorName != "github") return
variant.outputs.all { output ->
def builtType = variant.buildType.name
def versionName = variant.versionName
// def versionCode = variant.versionCode
def flavor = variant.flavorName
def flavorBuiltType = "${flavor}_${builtType}"
def suffix
// For x86 and x86_64, the versionNames are already overridden
if (versionName.contains(flavorBuiltType)) {
suffix = "${versionName}"
} else {
suffix = "${versionName}-${flavorBuiltType}" // eg. 19.1.0-github_debug or release
}
if (builtType.toString() == 'release' && project.hasProperty("pre")) {
buildConfigField("boolean", "isPre", "true")
flavorBuiltType = "${getGitHash()}-${flavor}"
// For x86 and x86_64, the versionNames are already overridden
if (versionName.contains(flavorBuiltType)) {
suffix = "${versionName}"
} else {
// append latest commit short hash for pre-release
suffix = "${versionName}.${flavorBuiltType}" // eg. 19.1.0.b123456-github
}
}
output.versionNameOverride = suffix
def abi = output.getFilter(com.android.build.OutputFile.ABI)
// println(abi + ", " + versionName + ", " + flavor + ", " + builtType + ", " + suffix)
outputFileName = abi == null ? "barinsta_${suffix}.apk" : "barinsta_${suffix}_${abi}.apk"
}
}
packagingOptions {
// Exclude file to avoid
// Error: Duplicate files during packaging of APK
exclude 'META-INF/LICENSE.md'
exclude 'META-INF/LICENSE-notice.md'
exclude 'META-INF/atomicfu.kotlin_module'
exclude 'META-INF/AL2.0'
exclude 'META-INF/LGPL2.1'
}
testOptions.unitTests {
includeAndroidResources = true
}
}
configurations.all {
resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
}
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
def exoplayer_version = '2.14.1'
implementation 'com.google.android.material:material:1.4.0'
implementation "com.google.android.exoplayer:exoplayer-core:$exoplayer_version"
implementation "com.google.android.exoplayer:exoplayer-dash:$exoplayer_version"
implementation "com.google.android.exoplayer:exoplayer-ui:$exoplayer_version"
implementation "androidx.recyclerview:recyclerview:1.2.1"
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation "androidx.viewpager2:viewpager2:1.0.0"
implementation "androidx.constraintlayout:constraintlayout:2.0.4"
implementation "androidx.preference:preference:1.1.1"
implementation 'androidx.palette:palette:1.0.0'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'com.google.guava:guava:27.0.1-android'
def core_version = "1.6.0"
implementation "androidx.core:core:$core_version"
// Fragment
implementation "androidx.fragment:fragment-ktx:1.3.5"
// Lifecycle
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"
// Navigation
implementation "androidx.navigation:navigation-fragment-ktx:$rootProject.nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$rootProject.nav_version"
// Room
def room_version = "2.3.0"
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-guava:$room_version"
implementation "androidx.room:room-ktx:$room_version"
kapt "androidx.room:room-compiler:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
// CameraX
def camerax_version = "1.1.0-alpha07"
implementation "androidx.camera:camera-camera2:$camerax_version"
implementation "androidx.camera:camera-lifecycle:$camerax_version"
implementation "androidx.camera:camera-view:1.0.0-alpha27"
// EmojiCompat
def emoji_compat_version = "1.1.0"
implementation "androidx.emoji:emoji:$emoji_compat_version"
implementation "androidx.emoji:emoji-appcompat:$emoji_compat_version"
// Work
def work_version = '2.5.0'
implementation "androidx.work:work-runtime:$work_version"
implementation "androidx.work:work-runtime-ktx:$work_version"
implementation "ru.gildor.coroutines:kotlin-coroutines-okhttp:1.0"
implementation 'com.facebook.fresco:fresco:2.5.0'
implementation 'com.facebook.fresco:animated-webp:2.5.0'
implementation 'com.facebook.fresco:webpsupport:2.5.0'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-scalars:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'org.apache.commons:commons-imaging:1.0-alpha2'
implementation 'com.github.skydoves:balloon:1.3.5'
implementation 'com.github.ammargitham:AutoLinkTextViewV2:3.2.0'
implementation 'com.github.ammargitham:uCrop:2.3-non-native'
implementation 'com.github.ammargitham:android-gpuimage:2.1.1-beta4'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
githubImplementation 'io.sentry:sentry-android:5.0.1'
testImplementation 'org.junit.jupiter:junit-jupiter:5.7.2'
testImplementation "androidx.test.ext:junit-ktx:1.1.3"
testImplementation "androidx.test:core-ktx:1.4.0"
testImplementation "androidx.arch.core:core-testing:2.1.0"
testImplementation "org.robolectric:robolectric:4.5.1"
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.0'
androidTestImplementation 'org.junit.jupiter:junit-jupiter:5.7.2'
androidTestImplementation 'androidx.test:core:1.4.0'
androidTestImplementation 'com.android.support:support-annotations:28.0.0'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation "androidx.room:room-testing:2.3.0"
androidTestImplementation "androidx.arch.core:core-testing:2.1.0"
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.0'
}
================================================
FILE: app/lint.xml
================================================
================================================
FILE: app/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
#noinspection ShrinkerUnresolvedReference
#-keep class !com.google.android.exoplayer2.**, ** { *; }
-dontobfuscate
# prevent shrinking retrofit response entities
-keep class awais.instagrabber.repositories.responses.** { *; }
================================================
FILE: app/schemas/awais.instagrabber.db.AppDatabase/4.json
================================================
{
"formatVersion": 1,
"database": {
"version": 4,
"identityHash": "538d64adaeb8c3a98db9204955932e59",
"entities": [
{
"tableName": "accounts",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uid` TEXT, `username` TEXT, `cookie` TEXT, `full_name` TEXT, `profile_pic` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "cookie",
"columnName": "cookie",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "fullName",
"columnName": "full_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "profilePic",
"columnName": "profile_pic",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "favorites",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query_text` TEXT, `type` TEXT, `display_name` TEXT, `pic_url` TEXT, `date_added` INTEGER)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "query",
"columnName": "query_text",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "displayName",
"columnName": "display_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "picUrl",
"columnName": "pic_url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "dateAdded",
"columnName": "date_added",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '538d64adaeb8c3a98db9204955932e59')"
]
}
}
================================================
FILE: app/schemas/awais.instagrabber.db.AppDatabase/5.json
================================================
{
"formatVersion": 1,
"database": {
"version": 5,
"identityHash": "0b38e12b76bb081ec837191c5ef5b54e",
"entities": [
{
"tableName": "accounts",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uid` TEXT, `username` TEXT, `cookie` TEXT, `full_name` TEXT, `profile_pic` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "cookie",
"columnName": "cookie",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "fullName",
"columnName": "full_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "profilePic",
"columnName": "profile_pic",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "favorites",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query_text` TEXT, `type` TEXT, `display_name` TEXT, `pic_url` TEXT, `date_added` INTEGER)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "query",
"columnName": "query_text",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "displayName",
"columnName": "display_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "picUrl",
"columnName": "pic_url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "dateAdded",
"columnName": "date_added",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "dm_last_notified",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `thread_id` TEXT, `last_notified_msg_ts` INTEGER, `last_notified_at` INTEGER)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "threadId",
"columnName": "thread_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastNotifiedMsgTs",
"columnName": "last_notified_msg_ts",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastNotifiedAt",
"columnName": "last_notified_at",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_dm_last_notified_thread_id",
"unique": true,
"columnNames": [
"thread_id"
],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_dm_last_notified_thread_id` ON `${TABLE_NAME}` (`thread_id`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0b38e12b76bb081ec837191c5ef5b54e')"
]
}
}
================================================
FILE: app/schemas/awais.instagrabber.db.AppDatabase/6.json
================================================
{
"formatVersion": 1,
"database": {
"version": 6,
"identityHash": "232e618b3bfcb4661336b359d036c455",
"entities": [
{
"tableName": "accounts",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uid` TEXT, `username` TEXT, `cookie` TEXT, `full_name` TEXT, `profile_pic` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "cookie",
"columnName": "cookie",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "fullName",
"columnName": "full_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "profilePic",
"columnName": "profile_pic",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "favorites",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query_text` TEXT, `type` TEXT, `display_name` TEXT, `pic_url` TEXT, `date_added` INTEGER)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "query",
"columnName": "query_text",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "displayName",
"columnName": "display_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "picUrl",
"columnName": "pic_url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "dateAdded",
"columnName": "date_added",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "dm_last_notified",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `thread_id` TEXT, `last_notified_msg_ts` INTEGER, `last_notified_at` INTEGER)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "threadId",
"columnName": "thread_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastNotifiedMsgTs",
"columnName": "last_notified_msg_ts",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastNotifiedAt",
"columnName": "last_notified_at",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_dm_last_notified_thread_id",
"unique": true,
"columnNames": [
"thread_id"
],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_dm_last_notified_thread_id` ON `${TABLE_NAME}` (`thread_id`)"
}
],
"foreignKeys": []
},
{
"tableName": "recent_searches",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ig_id` TEXT NOT NULL, `name` TEXT NOT NULL, `username` TEXT, `pic_url` TEXT, `type` TEXT NOT NULL, `last_searched_on` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "igId",
"columnName": "ig_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "picUrl",
"columnName": "pic_url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastSearchedOn",
"columnName": "last_searched_on",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_recent_searches_ig_id_type",
"unique": true,
"columnNames": [
"ig_id",
"type"
],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_recent_searches_ig_id_type` ON `${TABLE_NAME}` (`ig_id`, `type`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '232e618b3bfcb4661336b359d036c455')"
]
}
}
================================================
FILE: app/sentry.gradle
================================================
def dsnKey = 'DSN'
def defaultDsn = '\"\"'
final Properties properties = new Properties()
File propertiesFile = rootProject.file('sentry.properties')
if (!propertiesFile.exists()) {
propertiesFile.createNewFile()
}
properties.load(new FileInputStream(propertiesFile))
ext{
SENTRY_DSN = properties.getProperty(dsnKey, defaultDsn)
}
================================================
FILE: app/src/androidTest/java/awais/instagrabber/db/MigrationTest.java
================================================
package awais.instagrabber.db;
import androidx.room.Room;
import androidx.room.migration.Migration;
import androidx.room.testing.MigrationTestHelper;
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.IOException;
import static awais.instagrabber.db.AppDatabase.MIGRATION_4_5;
import static awais.instagrabber.db.AppDatabase.MIGRATION_5_6;
@RunWith(AndroidJUnit4.class)
public class MigrationTest {
private static final String TEST_DB = "migration-test";
private static final Migration[] ALL_MIGRATIONS = new Migration[]{MIGRATION_4_5, MIGRATION_5_6};
@Rule
public MigrationTestHelper helper;
public MigrationTest() {
final String canonicalName = AppDatabase.class.getCanonicalName();
helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(),
canonicalName,
new FrameworkSQLiteOpenHelperFactory());
}
@Test
public void migrateAll() throws IOException {
// Create earliest version of the database. Have to start with 4 since that is the version we migrated to Room.
SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 4);
db.close();
// Open latest version of the database. Room will validate the schema
// once all migrations execute.
AppDatabase appDb = Room.databaseBuilder(InstrumentationRegistry.getInstrumentation().getTargetContext(),
AppDatabase.class,
TEST_DB)
.addMigrations(ALL_MIGRATIONS).build();
appDb.getOpenHelper().getWritableDatabase();
appDb.close();
}
}
================================================
FILE: app/src/androidTest/java/awais/instagrabber/db/dao/RecentSearchDaoTest.kt
================================================
package awais.instagrabber.db.dao
import android.content.Context
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.runner.AndroidJUnit4
import awais.instagrabber.db.AppDatabase
import awais.instagrabber.db.entities.RecentSearch
import awais.instagrabber.models.enums.FavoriteType
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runBlockingTest
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.jupiter.api.Assertions
import org.junit.runner.RunWith
import java.time.LocalDateTime
@RunWith(AndroidJUnit4::class)
class RecentSearchDaoTest {
private lateinit var db: AppDatabase
private lateinit var dao: RecentSearchDao
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
@Before
fun createDb() {
val context = ApplicationProvider.getApplicationContext()
db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java).build()
dao = db.recentSearchDao()
}
@After
fun closeDb() {
db.close()
}
@ExperimentalCoroutinesApi
@Test
fun writeQueryDelete() = runBlockingTest {
val recentSearch = insertRecentSearch(1, "1", "test1", FavoriteType.HASHTAG)
val byIgIdAndType = dao.getRecentSearchByIgIdAndType("1", FavoriteType.HASHTAG)
Assertions.assertNotNull(byIgIdAndType)
Assertions.assertEquals(recentSearch, byIgIdAndType)
dao.deleteRecentSearch(byIgIdAndType ?: throw NullPointerException())
val deleted = dao.getRecentSearchByIgIdAndType("1", FavoriteType.HASHTAG)
Assertions.assertNull(deleted)
}
@ExperimentalCoroutinesApi
@Test
fun queryAllOrdered() = runBlockingTest {
val insertListReversed: List = listOf(
insertRecentSearch(1, "1", "test1", FavoriteType.HASHTAG),
insertRecentSearch(2, "2", "test2", FavoriteType.LOCATION),
insertRecentSearch(3, "3", "test3", FavoriteType.USER),
insertRecentSearch(4, "4", "test4", FavoriteType.USER),
insertRecentSearch(5, "5", "test5", FavoriteType.USER)
).asReversed() // important
val fromDb: List = dao.getAllRecentSearches()
Assertions.assertIterableEquals(insertListReversed, fromDb)
}
private fun insertRecentSearch(id: Int, igId: String, name: String, type: FavoriteType): RecentSearch {
val recentSearch = RecentSearch(
id,
igId,
name,
null,
null,
type,
LocalDateTime.now()
)
runBlocking { dao.insertRecentSearch(recentSearch) }
return recentSearch
}
}
================================================
FILE: app/src/androidTest/java/awaisomereport/CrashReporterHelperTest.kt
================================================
package awaisomereport
import androidx.test.runner.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
internal class CrashReporterHelperTest {
@Test
fun getErrorContent() {
val errorContent = CrashReporterHelper.getReportContent(Exception())
print(errorContent)
}
}
================================================
FILE: app/src/fdroid/java/awais/instagrabber/fragments/settings/FlavorSettings.java
================================================
package awais.instagrabber.fragments.settings;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.fragment.app.FragmentManager;
import androidx.preference.Preference;
import java.util.Collections;
import java.util.List;
import awais.instagrabber.fragments.settings.IFlavorSettings;
import awais.instagrabber.fragments.settings.SettingCategory;
public final class FlavorSettings implements IFlavorSettings {
private static FlavorSettings instance;
private FlavorSettings() {
}
public static FlavorSettings getInstance() {
if (instance == null) {
instance = new FlavorSettings();
}
return instance;
}
@NonNull
@Override
public List getPreferences(@NonNull final Context context,
@NonNull final FragmentManager fragmentManager,
@NonNull final SettingCategory settingCategory) {
// switch (settingCategory) {
// default:
// break;
// }
return Collections.emptyList();
}
}
================================================
FILE: app/src/fdroid/java/awais/instagrabber/utils/UpdateChecker.java
================================================
package awais.instagrabber.utils;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import org.json.JSONObject;
import java.net.HttpURLConnection;
import java.net.URL;
public class UpdateChecker {
private static final Object LOCK = new Object();
private static final String TAG = UpdateChecker.class.getSimpleName();
private static UpdateChecker instance;
public static UpdateChecker getInstance() {
if (instance == null) {
synchronized (LOCK) {
if (instance == null) {
instance = new UpdateChecker();
}
}
}
return instance;
}
/**
* Needs to be called asynchronously
*
* @return the latest version from f-droid
*/
@Nullable
public String getLatestVersion() {
HttpURLConnection conn = null;
try {
conn = (HttpURLConnection) new URL("https://f-droid.org/api/v1/packages/me.austinhuang.instagrabber").openConnection();
conn.setUseCaches(false);
conn.setRequestProperty("User-Agent", "https://Barinsta.AustinHuang.me / mailto:Barinsta@AustinHuang.me");
conn.connect();
final int responseCode = conn.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
final JSONObject data = new JSONObject(NetworkUtils.readFromConnection(conn));
return "v" + data.getJSONArray("packages").getJSONObject(0).getString("versionName");
// if (BuildConfig.VERSION_CODE < data.getInt("suggestedVersionCode")) {
// }
}
} catch (final Exception e) {
Log.e(TAG, "", e);
} finally {
if (conn != null) {
conn.disconnect();
}
}
return null;
}
public void onDownload(@NonNull final AppCompatActivity context) {
Utils.openURL(context, "https://f-droid.org/packages/me.austinhuang.instagrabber/");
}
}
================================================
FILE: app/src/fdroid/java/awaisomereport/CrashHandler.kt
================================================
package awaisomereport
import android.app.Application
class CrashHandler(private val application: Application) : ICrashHandler {
override fun uncaughtException(
t: Thread,
exception: Throwable,
defaultExceptionHandler: Thread.UncaughtExceptionHandler
) {
CrashReporterHelper.startErrorReporterActivity(application, exception)
defaultExceptionHandler.uncaughtException(t, exception)
}
}
================================================
FILE: app/src/github/AndroidManifest.xml
================================================
================================================
FILE: app/src/github/java/awais/instagrabber/fragments/settings/FlavorSettings.java
================================================
package awais.instagrabber.fragments.settings;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.fragment.app.FragmentManager;
import androidx.preference.Preference;
import com.google.common.collect.ImmutableList;
import java.util.Collections;
import java.util.List;
import awais.instagrabber.R;
import awais.instagrabber.dialogs.ConfirmDialogFragment;
import static awais.instagrabber.fragments.settings.PreferenceKeys.PREF_ENABLE_SENTRY;
import static awais.instagrabber.utils.Utils.settingsHelper;
public final class FlavorSettings implements IFlavorSettings {
private static FlavorSettings instance;
private FlavorSettings() {
}
public static FlavorSettings getInstance() {
if (instance == null) {
instance = new FlavorSettings();
}
return instance;
}
@NonNull
@Override
public List getPreferences(@NonNull final Context context,
@NonNull final FragmentManager fragmentManager,
@NonNull final SettingCategory settingCategory) {
switch (settingCategory) {
case GENERAL:
return getGeneralPrefs(context, fragmentManager);
default:
break;
}
return Collections.emptyList();
}
private List getGeneralPrefs(@NonNull final Context context,
@NonNull final FragmentManager fragmentManager) {
return ImmutableList.of(
getSentryPreference(context, fragmentManager)
);
}
private Preference getSentryPreference(@NonNull final Context context,
@NonNull final FragmentManager fragmentManager) {
if (!settingsHelper.hasPreference(PREF_ENABLE_SENTRY)) {
// disabled by default
settingsHelper.putBoolean(PREF_ENABLE_SENTRY, false);
}
return PreferenceHelper.getSwitchPreference(
context,
PREF_ENABLE_SENTRY,
R.string.enable_sentry,
R.string.sentry_summary,
false,
(preference, newValue) -> {
if (!(newValue instanceof Boolean)) return true;
final boolean enabled = (Boolean) newValue;
if (enabled) {
final ConfirmDialogFragment dialogFragment = ConfirmDialogFragment.newInstance(
111,
0,
R.string.sentry_start_next_launch,
R.string.ok,
0,
0);
dialogFragment.show(fragmentManager, "sentry_dialog");
}
return true;
});
}
}
================================================
FILE: app/src/github/java/awais/instagrabber/utils/UpdateChecker.java
================================================
package awais.instagrabber.utils;
import android.content.Context;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.net.HttpURLConnection;
import java.net.URL;
public class UpdateChecker {
private static final Object LOCK = new Object();
private static final String TAG = UpdateChecker.class.getSimpleName();
private static UpdateChecker instance;
public static UpdateChecker getInstance() {
if (instance == null) {
synchronized (LOCK) {
if (instance == null) {
instance = new UpdateChecker();
}
}
}
return instance;
}
/**
* Needs to be called asynchronously
*
* @return the latest version from Github
*/
@Nullable
public String getLatestVersion() {
HttpURLConnection conn = null;
try {
conn = (HttpURLConnection) new URL("https://github.com/austinhuang0131/barinsta/releases/latest").openConnection();
conn.setInstanceFollowRedirects(false);
conn.setUseCaches(false);
conn.setRequestProperty("User-Agent", "https://Barinsta.AustinHuang.me / mailto:Barinsta@AustinHuang.me");
conn.connect();
final int responseCode = conn.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_MOVED_TEMP) {
return "v" + conn.getHeaderField("Location").split("/v")[1];
// return !version.equals(BuildConfig.VERSION_NAME);
}
} catch (final Exception e) {
Log.e(TAG, "", e);
} finally {
if (conn != null) {
conn.disconnect();
}
}
return null;
}
public void onDownload(@NonNull final Context context) {
Utils.openURL(context, "https://github.com/austinhuang0131/instagrabber/releases/latest");
}
}
================================================
FILE: app/src/github/java/awaisomereport/CrashHandler.kt
================================================
package awaisomereport
import android.app.Application
import awais.instagrabber.BuildConfig
import awais.instagrabber.fragments.settings.PreferenceKeys
import awais.instagrabber.utils.Utils
import io.sentry.SentryEvent
import io.sentry.SentryLevel
import io.sentry.SentryOptions.BeforeSendCallback
import io.sentry.android.core.SentryAndroid
import io.sentry.android.core.SentryAndroidOptions
class CrashHandler(private val application: Application) : ICrashHandler {
private var enabled = false
init {
enabled = if (!Utils.settingsHelper.hasPreference(PreferenceKeys.PREF_ENABLE_SENTRY)) {
// disabled by default (change to true if we need enabled by default)
false
} else {
Utils.settingsHelper.getBoolean(PreferenceKeys.PREF_ENABLE_SENTRY)
}
if (enabled) {
SentryAndroid.init(application) { options: SentryAndroidOptions ->
options.dsn = BuildConfig.dsn
options.setDiagnosticLevel(SentryLevel.ERROR)
options.beforeSend = BeforeSendCallback { event: SentryEvent, _: Any? ->
// Removing unneeded info from event
event.contexts.device?.apply {
name = null
timezone = null
isCharging = null
bootTime = null
freeStorage = null
batteryTemperature = null
}
event
}
}
}
}
override fun uncaughtException(
t: Thread,
exception: Throwable,
defaultExceptionHandler: Thread.UncaughtExceptionHandler
) {
// When enabled, Sentry auto captures unhandled exceptions
if (!enabled) {
CrashReporterHelper.startErrorReporterActivity(application, exception)
}
defaultExceptionHandler.uncaughtException(t, exception)
}
}
================================================
FILE: app/src/github/res/values/strings.xml
================================================
Enable Sentry
Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io
Sentry will start on next launch
================================================
FILE: app/src/github/res/values-ar/strings.xml
================================================
تمكين الحراسة
الحراسة هي مستمع/معالج للأخطاء الذي يرسل الخطأ/الاحداث إلى Sentry.io
ستبدأ الحراسة عند التشغيل التالي
================================================
FILE: app/src/github/res/values-ca/strings.xml
================================================
Habilita el Sentry
Sentry és un oient/intèrpret d\'error que envia asíncronament l\'error/esdeveniment a Sentry.io
Sentry s\'iniciarà al pròxim llançament
================================================
FILE: app/src/github/res/values-cs/strings.xml
================================================
Povolit Sentry
Sentry je listener/handler, který zaznamenává chyby a asynchronně je posílá na Sentry.io
Sentry se spustí při příštím spuštění
================================================
FILE: app/src/github/res/values-de/strings.xml
================================================
Aktiviere Sentry
Sentry ist ein Listener/Handler für Fehler, der den Fehler/das Ereignis asynchron an Sentry.io sendet
Sentry startet beim nächsten Start
================================================
FILE: app/src/github/res/values-el/strings.xml
================================================
Ενεργοποίηση Sentry
Το Sentry είναι διαχειριστής σφαλμάτων ασύγχρονης αποστολής του σφάλματος/συμβάντος στο Sentry.io
Το Sentry θα ξεκινήσει στην επόμενη εκκίνηση
================================================
FILE: app/src/github/res/values-es/strings.xml
================================================
Activar Sentry
Sentry es un oyente/manejador de errores que asincrónicamente envía el error/evento a Sentry.io
Sentry comenzará en el próximo inicio
================================================
FILE: app/src/github/res/values-eu/strings.xml
================================================
Enable Sentry
Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io
Sentry will start on next launch
================================================
FILE: app/src/github/res/values-fa/strings.xml
================================================
فعالسازی Sentry
Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io
Sentry در اجرای بعدی، شروع خواهد شد
================================================
FILE: app/src/github/res/values-fr/strings.xml
================================================
Activer Sentry
Sentry est un écouteur/gestionnaire d\'erreurs qui envoie de manière asynchrone l\'erreur/l\'événement à Sentry.io
Sentry commencera au prochain lancement
================================================
FILE: app/src/github/res/values-hi/strings.xml
================================================
Enable Sentry
Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io
Sentry will start on next launch
================================================
FILE: app/src/github/res/values-in/strings.xml
================================================
Hidupkan Sentry
Sentry adalah sebuah pendengar/penanganan eror yang secara asinkronis mengirimkan eror/kejadian ke Sentry.io
Sentry akan dihidupkan pada peluncuran berikutnya
================================================
FILE: app/src/github/res/values-it/strings.xml
================================================
Abilita Sentry
Sentry è un ascoltatore/gestore di errori che invia asincronicamente l\'errore/evento a Sentry.io
Sentry comincerà al prossimo lancio
================================================
FILE: app/src/github/res/values-ja/strings.xml
================================================
Enable Sentry
Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io
Sentry will start on next launch
================================================
FILE: app/src/github/res/values-ko/strings.xml
================================================
Sentry 활성화
Sentry는 Sentry.io에 오류를 비동기적으로 보내는 오류 처리기입니다
Sentry는 다음 출시에 시작됩니다
================================================
FILE: app/src/github/res/values-mk/strings.xml
================================================
Овозможи Sentry
Sentry е слушач на грешки кој асинхроно ги испраќа на Sentry.io страната
Sentry ќе биде овозможен на следно отварање
================================================
FILE: app/src/github/res/values-nl/strings.xml
================================================
Sentry inschakelen
Sentry is een luister/handler voor fouten die asynchroon de fout/gebeurtenis versturen naar Sentry.io
Sentry zal starten bij de volgende lancering
================================================
FILE: app/src/github/res/values-or/strings.xml
================================================
Sentryକୁ ସକ୍ଷମ କରନ୍ତୁ
ତ୍ରୁଟି ପାଇଁ ସେଣ୍ଟ୍ରି ହେଉଛି ଏକ ଶ୍ରୋତା ଯାହା ତ୍ରୁଟି / ଘଟଣାକୁ Sentry.io କୁ ପଠାଏ |
ପରବର୍ତ୍ତୀ ଲଞ୍ଚଠାରୁ ସେଣ୍ଟ୍ରି ଆରମ୍ଭ ହେବ |
================================================
FILE: app/src/github/res/values-pl/strings.xml
================================================
Włącz Sentry
Sentry jest słuchaczem/obsługą błędów, które asynchronicznie wysyłają błąd/zdarzenie do Sentry.io
Sentry rozpocznie się przy następnym uruchomieniu
================================================
FILE: app/src/github/res/values-pt/strings.xml
================================================
Ativar Sentry
Sentry é um ouvinte/gestor de erros que assincronicamente envia o erro/evento para Sentry.io
Sentry começará no próximo início
================================================
FILE: app/src/github/res/values-ru/strings.xml
================================================
Включить Sentry
Sentry - это обработчик событий, который асинхронно отправляет сообщения об ошибках/поломках на Sentry.io
Sentry включится при следующем запуске приложения
================================================
FILE: app/src/github/res/values-sk/strings.xml
================================================
Enable Sentry
Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io
Sentry will start on next launch
================================================
FILE: app/src/github/res/values-sv/strings.xml
================================================
Enable Sentry
Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io
Sentry will start on next launch
================================================
FILE: app/src/github/res/values-tr/strings.xml
================================================
Enable Sentry
Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io
Sentry will start on next launch
================================================
FILE: app/src/github/res/values-vi/strings.xml
================================================
Bật Sentry
Sentry là một thiết bị nghe/giải quyết cho những lỗi mà gửi những lỗi/sự kiện đến Sentry.io một cách tách biệt
Sentry sẽ được bật vào lần khởi động kế tiếp
================================================
FILE: app/src/github/res/values-zh-rCN/strings.xml
================================================
启用 Sentry
Sentry 会将错误报告发送至 Sentry.io
启用 Sentry 将在下次启动应用时生效
================================================
FILE: app/src/github/res/values-zh-rTW/strings.xml
================================================
啟用 Sentry
Sentry 會將錯誤報告發送至 Sentry.io
下次啟用應用程式時將會開啟 Sentry
================================================
FILE: app/src/main/AndroidManifest.xml
================================================
================================================
FILE: app/src/main/java/awais/instagrabber/InstaGrabberApplication.kt
================================================
package awais.instagrabber
import android.app.Application
import android.content.ClipboardManager
import android.util.Log
import awais.instagrabber.fragments.settings.PreferenceKeys.CUSTOM_DATE_TIME_FORMAT
import awais.instagrabber.fragments.settings.PreferenceKeys.CUSTOM_DATE_TIME_FORMAT_ENABLED
import awais.instagrabber.fragments.settings.PreferenceKeys.DATE_TIME_FORMAT
import awais.instagrabber.utils.*
import awais.instagrabber.utils.LocaleUtils.currentLocale
import awais.instagrabber.utils.Utils.settingsHelper
import awais.instagrabber.utils.extensions.TAG
import awaisomereport.CrashReporter
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.imagepipeline.core.ImagePipelineConfig
import java.net.CookieHandler
import java.time.format.DateTimeFormatter
import java.util.*
@Suppress("unused")
class InstaGrabberApplication : Application() {
override fun onCreate() {
super.onCreate()
CookieHandler.setDefault(NET_COOKIE_MANAGER)
settingsHelper = SettingsHelper(this)
setupCrashReporter()
setupCloseGuard()
setupFresco()
Utils.cacheDir = cacheDir.absolutePath
Utils.clipboardManager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
LocaleUtils.setLocale(baseContext)
val pattern = if (settingsHelper.getBoolean(CUSTOM_DATE_TIME_FORMAT_ENABLED)) {
settingsHelper.getString(CUSTOM_DATE_TIME_FORMAT)
} else {
settingsHelper.getString(DATE_TIME_FORMAT)
}
TextUtils.setFormatter(DateTimeFormatter.ofPattern(pattern, currentLocale))
if (TextUtils.isEmpty(settingsHelper.getString(Constants.DEVICE_UUID))) {
settingsHelper.putString(Constants.DEVICE_UUID, UUID.randomUUID().toString())
}
}
private fun setupCrashReporter() {
if (BuildConfig.DEBUG) return
CrashReporter.getInstance(this).start()
}
private fun setupCloseGuard() {
if (!BuildConfig.DEBUG) return
try {
Class.forName("dalvik.system.CloseGuard")
.getMethod("setEnabled", Boolean::class.javaPrimitiveType)
.invoke(null, true)
} catch (e: Exception) {
Log.e(TAG, "Error", e)
}
}
private fun setupFresco() {
// final Set requestListeners = new HashSet<>();
// requestListeners.add(new RequestLoggingListener());
val imagePipelineConfig = ImagePipelineConfig
.newBuilder(this) // .setMainDiskCacheConfig(diskCacheConfig)
// .setRequestListeners(requestListeners)
.setDownsampleEnabled(true)
.build()
Fresco.initialize(this, imagePipelineConfig)
// FLog.setMinimumLoggingLevel(FLog.VERBOSE);
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/activities/BaseLanguageActivity.kt
================================================
package awais.instagrabber.activities
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import awais.instagrabber.utils.LocaleUtils
import awais.instagrabber.utils.ThemeUtils
abstract class BaseLanguageActivity protected constructor() : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
ThemeUtils.changeTheme(this)
super.onCreate(savedInstanceState)
}
init {
@Suppress("LeakingThis")
LocaleUtils.updateConfig(this)
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/activities/CameraActivity.kt
================================================
package awais.instagrabber.activities
import android.content.Intent
import android.content.res.Configuration
import android.hardware.display.DisplayManager
import android.hardware.display.DisplayManager.DisplayListener
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import androidx.documentfile.provider.DocumentFile
import awais.instagrabber.databinding.ActivityCameraBinding
import awais.instagrabber.utils.DownloadUtils
import awais.instagrabber.utils.PermissionUtils
import awais.instagrabber.utils.Utils
import awais.instagrabber.utils.extensions.TAG
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ExecutionException
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
class CameraActivity : BaseLanguageActivity() {
private lateinit var binding: ActivityCameraBinding
private lateinit var displayManager: DisplayManager
private lateinit var cameraExecutor: ExecutorService
private var outputDirectory: DocumentFile? = null
private var imageCapture: ImageCapture? = null
private var displayId = -1
private var cameraProvider: ProcessCameraProvider? = null
private var lensFacing = 0
private val cameraRequestCode = 100
private val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US)
private val displayListener: DisplayListener = object : DisplayListener {
override fun onDisplayAdded(displayId: Int) {}
override fun onDisplayRemoved(displayId: Int) {}
override fun onDisplayChanged(displayId: Int) {
if (displayId == this@CameraActivity.displayId) {
imageCapture?.targetRotation = binding.root.display.rotation
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityCameraBinding.inflate(LayoutInflater.from(baseContext))
setContentView(binding.root)
Utils.transparentStatusBar(this, true, false)
displayManager = getSystemService(DISPLAY_SERVICE) as DisplayManager
outputDirectory = DownloadUtils.cameraDir
cameraExecutor = Executors.newSingleThreadExecutor()
displayManager.registerDisplayListener(displayListener, null)
binding.viewFinder.post {
displayId = binding.viewFinder.display.displayId
updateUi()
checkPermissionsAndSetupCamera()
}
}
override fun onResume() {
super.onResume()
// Make sure that all permissions are still present, since the
// user could have removed them while the app was in paused state.
if (!PermissionUtils.hasCameraPerms(this)) {
PermissionUtils.requestCameraPerms(this, cameraRequestCode)
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
// Redraw the camera UI controls
updateUi()
// Enable or disable switching between cameras
updateCameraSwitchButton()
}
override fun onDestroy() {
super.onDestroy()
Utils.transparentStatusBar(this, false, false)
cameraExecutor.shutdown()
displayManager.unregisterDisplayListener(displayListener)
}
private fun updateUi() {
binding.cameraCaptureButton.setOnClickListener { takePhoto() }
// Disable the button until the camera is set up
binding.switchCamera.isEnabled = false
// Listener for button used to switch cameras. Only called if the button is enabled
binding.switchCamera.setOnClickListener {
lensFacing = if (CameraSelector.LENS_FACING_FRONT == lensFacing) CameraSelector.LENS_FACING_BACK else CameraSelector.LENS_FACING_FRONT
// Re-bind use cases to update selected camera
bindCameraUseCases()
}
binding.close.setOnClickListener {
setResult(RESULT_CANCELED)
finish()
}
}
private fun checkPermissionsAndSetupCamera() {
if (PermissionUtils.hasCameraPerms(this)) {
setupCamera()
return
}
PermissionUtils.requestCameraPerms(this, cameraRequestCode)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == cameraRequestCode) {
if (PermissionUtils.hasCameraPerms(this)) {
setupCamera()
}
}
}
private fun setupCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
try {
cameraProvider = cameraProviderFuture.get()
// Select lensFacing depending on the available cameras
lensFacing = -1
if (hasBackCamera()) {
lensFacing = CameraSelector.LENS_FACING_BACK
} else if (hasFrontCamera()) {
lensFacing = CameraSelector.LENS_FACING_FRONT
}
check(lensFacing != -1) { "Back and front camera are unavailable" }
// Enable or disable switching between cameras
updateCameraSwitchButton()
// Build and bind the camera use cases
bindCameraUseCases()
} catch (e: ExecutionException) {
Log.e(TAG, "setupCamera: ", e)
} catch (e: InterruptedException) {
Log.e(TAG, "setupCamera: ", e)
} catch (e: CameraInfoUnavailableException) {
Log.e(TAG, "setupCamera: ", e)
}
}, ContextCompat.getMainExecutor(this))
}
private fun bindCameraUseCases() {
val rotation = binding.viewFinder.display.rotation
// CameraSelector
val cameraSelector = CameraSelector.Builder()
.requireLensFacing(lensFacing)
.build()
// Preview
val preview = Preview.Builder() // Set initial target rotation
.setTargetRotation(rotation)
.build()
// ImageCapture
imageCapture = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) // Set initial target rotation, we will have to call this again if rotation changes
// during the lifecycle of this use case
.setTargetRotation(rotation)
.build()
cameraProvider?.unbindAll()
cameraProvider?.bindToLifecycle(this, cameraSelector, preview, imageCapture)
preview.setSurfaceProvider(binding.viewFinder.surfaceProvider)
}
private fun takePhoto() {
if (imageCapture == null) return
val fileName = simpleDateFormat.format(System.currentTimeMillis()) + ".jpg"
val mimeType = "image/jpg"
val photoFile = outputDirectory?.createFile(mimeType, fileName)?.let { it } ?: return
val outputStream = contentResolver.openOutputStream(photoFile.uri)?.let { it } ?: return
val outputFileOptions = ImageCapture.OutputFileOptions.Builder(outputStream).build()
imageCapture?.takePicture(
outputFileOptions,
cameraExecutor,
object : ImageCapture.OnImageSavedCallback {
@Suppress("UnstableApiUsage")
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
try { outputStream.close() } catch (ignored: IOException) {}
val intent = Intent()
intent.data = photoFile.uri
setResult(RESULT_OK, intent)
finish()
Log.d(TAG, "onImageSaved: " + photoFile.uri)
}
override fun onError(exception: ImageCaptureException) {
Log.e(TAG, "onError: ", exception)
try { outputStream.close() } catch (ignored: IOException) {}
}
}
)
// We can only change the foreground Drawable using API level 23+ API
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// // Display flash animation to indicate that photo was captured
// final ConstraintLayout container = binding.getRoot();
// container.postDelayed(() -> {
// container.setForeground(new ColorDrawable(Color.WHITE));
// container.postDelayed(() -> container.setForeground(null), 50);
// }, 100);
// }
}
/**
* Enabled or disabled a button to switch cameras depending on the available cameras
*/
private fun updateCameraSwitchButton() {
try {
binding.switchCamera.isEnabled = hasBackCamera() && hasFrontCamera()
} catch (e: CameraInfoUnavailableException) {
binding.switchCamera.isEnabled = false
}
}
/**
* Returns true if the device has an available back camera. False otherwise
*/
@Throws(CameraInfoUnavailableException::class)
private fun hasBackCamera(): Boolean {
return if (cameraProvider == null) false else cameraProvider?.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA) ?: false
}
/**
* Returns true if the device has an available front camera. False otherwise
*/
@Throws(CameraInfoUnavailableException::class)
private fun hasFrontCamera(): Boolean {
return if (cameraProvider == null) {
false
} else cameraProvider?.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA) ?: false
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/activities/DirectorySelectActivity.kt
================================================
package awais.instagrabber.activities
import android.annotation.SuppressLint
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.DocumentsContract
import android.util.Log
import android.view.View
import androidx.activity.viewModels
import awais.instagrabber.R
import awais.instagrabber.databinding.ActivityDirectorySelectBinding
import awais.instagrabber.dialogs.ConfirmDialogFragment
import awais.instagrabber.utils.AppExecutors.mainThread
import awais.instagrabber.utils.Constants
import awais.instagrabber.utils.extensions.TAG
import awais.instagrabber.viewmodels.DirectorySelectActivityViewModel
import java.io.IOException
import java.io.PrintWriter
import java.io.StringWriter
class DirectorySelectActivity : BaseLanguageActivity() {
private var initialUri: Uri? = null
private lateinit var binding: ActivityDirectorySelectBinding
private val viewModel: DirectorySelectActivityViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityDirectorySelectBinding.inflate(layoutInflater)
setContentView(binding.root)
val intent = intent
viewModel.setInitialUri(intent)
setupObservers()
binding.selectDir.setOnClickListener { openDirectoryChooser() }
initialUri = intent.getParcelableExtra(Constants.EXTRA_INITIAL_URI)
}
private fun setupObservers() {
viewModel.message.observe(this, { message: String? -> binding.message.text = message })
viewModel.prevUri.observe(this, { prevUri: String? ->
if (prevUri == null) {
binding.prevUri.visibility = View.GONE
binding.message2.visibility = View.GONE
return@observe
}
binding.prevUri.text = prevUri
binding.prevUri.visibility = View.VISIBLE
binding.message2.visibility = View.VISIBLE
})
viewModel.dirSuccess.observe(this, { success: Boolean -> binding.selectDir.visibility = if (success) View.GONE else View.VISIBLE })
viewModel.loading.observe(this, { loading: Boolean ->
binding.message.visibility = if (loading) View.GONE else View.VISIBLE
binding.loadingIndicator.visibility = if (loading) View.VISIBLE else View.GONE
})
}
private fun openDirectoryChooser() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && initialUri != null) {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, initialUri)
}
try {
startActivityForResult(intent, SELECT_DIR_REQUEST_CODE)
} catch (e: ActivityNotFoundException) {
Log.e(TAG, "openDirectoryChooser: ", e)
showErrorDialog(getString(R.string.no_directory_picker_activity))
} catch (e: Exception) {
Log.e(TAG, "openDirectoryChooser: ", e)
}
}
@SuppressLint("StringFormatInvalid")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode != SELECT_DIR_REQUEST_CODE) return
if (resultCode != RESULT_OK) {
showErrorDialog(getString(R.string.select_a_folder))
return
}
if (data == null || data.data == null) {
showErrorDialog(getString(R.string.select_a_folder))
return
}
val authority = data.data?.authority
if ("com.android.externalstorage.documents" != authority) {
showErrorDialog(getString(R.string.dir_select_no_download_folder, authority))
return
}
mainThread.execute({
try {
viewModel.setupSelectedDir(data)
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
finish()
} catch (e: Exception) {
// Should not come to this point.
// If it does, we have to show this error to the user so that they can report it.
try {
StringWriter().use { sw ->
PrintWriter(sw).use { pw ->
e.printStackTrace(pw)
showErrorDialog("Please report this error to the developers:\n\n$sw")
}
}
} catch (ioException: IOException) {
Log.e(TAG, "onActivityResult: ", ioException)
}
}
}, 500)
}
private fun showErrorDialog(message: String) {
val dialogFragment = ConfirmDialogFragment.newInstance(
ERROR_REQUEST_CODE,
R.string.error,
message,
R.string.ok,
0,
0
)
dialogFragment.show(supportFragmentManager, ConfirmDialogFragment::class.java.simpleName)
}
companion object {
const val SELECT_DIR_REQUEST_CODE = 0x01
private const val ERROR_REQUEST_CODE = 0x02
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/activities/Login.kt
================================================
package awais.instagrabber.activities
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Bitmap
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.webkit.*
import android.widget.Toast
import awais.instagrabber.R
import awais.instagrabber.databinding.ActivityLoginBinding
import awais.instagrabber.utils.Constants
import awais.instagrabber.utils.getCookie
class Login : BaseLanguageActivity(), View.OnClickListener {
private var webViewUrl: String? = null
private var ready = false
private lateinit var loginBinding: ActivityLoginBinding
private val webChromeClient = WebChromeClient()
private val webViewClient: WebViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
webViewUrl = url
}
override fun onPageFinished(view: WebView, url: String) {
webViewUrl = url
val mainCookie = getCookie(url)
if (mainCookie.isNullOrBlank() || !mainCookie.contains("; ds_user_id=")) {
ready = true
return
}
if (mainCookie.contains("; ds_user_id=") && ready) {
returnCookieResult(mainCookie)
}
}
}
private fun returnCookieResult(mainCookie: String?) {
val intent = Intent()
intent.putExtra("cookie", mainCookie)
setResult(Constants.LOGIN_RESULT_CODE, intent)
finish()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
loginBinding = ActivityLoginBinding.inflate(LayoutInflater.from(applicationContext))
setContentView(loginBinding.root)
initWebView()
loginBinding.cookies.setOnClickListener(this)
loginBinding.refresh.setOnClickListener(this)
}
override fun onClick(v: View) {
if (v === loginBinding.refresh) {
loginBinding.webView.loadUrl("https://instagram.com/")
return
}
if (v === loginBinding.cookies) {
val mainCookie = getCookie(webViewUrl)
if (mainCookie.isNullOrBlank() || !mainCookie.contains("; ds_user_id=")) {
Toast.makeText(this, R.string.login_error_loading_cookies, Toast.LENGTH_SHORT).show()
return
}
returnCookieResult(mainCookie)
}
}
@SuppressLint("SetJavaScriptEnabled")
private fun initWebView() {
loginBinding.webView.webChromeClient = webChromeClient
loginBinding.webView.webViewClient = webViewClient
val webSettings = loginBinding.webView.settings
webSettings.userAgentString =
"Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.105 Mobile Safari/537.36"
webSettings.javaScriptEnabled = true
webSettings.domStorageEnabled = true
webSettings.setSupportZoom(true)
webSettings.builtInZoomControls = true
webSettings.displayZoomControls = false
webSettings.loadWithOverviewMode = true
webSettings.useWideViewPort = true
webSettings.allowFileAccessFromFileURLs = true
webSettings.allowUniversalAccessFromFileURLs = true
webSettings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
CookieManager.getInstance().removeAllCookies(null)
CookieManager.getInstance().flush()
} else {
val cookieSyncMngr = CookieSyncManager.createInstance(applicationContext)
cookieSyncMngr.startSync()
val cookieManager = CookieManager.getInstance()
cookieManager.removeAllCookie()
cookieManager.removeSessionCookie()
cookieSyncMngr.stopSync()
cookieSyncMngr.sync()
}
loginBinding.webView.loadUrl("https://instagram.com/")
}
override fun onPause() {
loginBinding.webView.onPause()
super.onPause()
}
override fun onResume() {
super.onResume()
loginBinding.webView.onResume()
}
override fun onDestroy() {
loginBinding.webView.destroy()
super.onDestroy()
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/activities/MainActivity.kt
================================================
package awais.instagrabber.activities
import android.animation.LayoutTransition
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.os.*
import android.provider.DocumentsContract.EXTRA_INITIAL_URI
import android.text.Editable
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.WindowManager
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.app.NotificationManagerCompat
import androidx.core.provider.FontRequest
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.emoji.text.EmojiCompat
import androidx.emoji.text.EmojiCompat.InitCallback
import androidx.emoji.text.FontRequestEmojiCompatConfig
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.NavGraph
import androidx.navigation.NavGraphNavigator
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.*
import awais.instagrabber.BuildConfig
import awais.instagrabber.R
import awais.instagrabber.customviews.emoji.EmojiVariantManager
import awais.instagrabber.customviews.helpers.RootViewDeferringInsetsCallback
import awais.instagrabber.customviews.helpers.TextWatcherAdapter
import awais.instagrabber.databinding.ActivityMainBinding
import awais.instagrabber.fragments.main.FeedFragment
import awais.instagrabber.fragments.settings.PreferenceKeys
import awais.instagrabber.models.IntentModel
import awais.instagrabber.models.Resource
import awais.instagrabber.models.Tab
import awais.instagrabber.models.enums.IntentModelType
import awais.instagrabber.services.ActivityCheckerService
import awais.instagrabber.services.DMSyncAlarmReceiver
import awais.instagrabber.utils.*
import awais.instagrabber.utils.AppExecutors.tasksThread
import awais.instagrabber.utils.DownloadUtils.ReselectDocumentTreeException
import awais.instagrabber.utils.TextUtils.isEmpty
import awais.instagrabber.utils.emoji.EmojiParser
import awais.instagrabber.viewmodels.AppStateViewModel
import awais.instagrabber.viewmodels.DirectInboxViewModel
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.AppBarLayout.ScrollingViewBehavior
import com.google.android.material.appbar.CollapsingToolbarLayout
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.textfield.TextInputLayout
import com.google.common.collect.ImmutableList
import java.util.*
class MainActivity : BaseLanguageActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var navController: NavController
private lateinit var appBarConfiguration: AppBarConfiguration
private var searchMenuItem: MenuItem? = null
private var startNavRootId: Int = 0
private var lastSelectedNavMenuId = 0
private var isActivityCheckerServiceBound = false
private var isLoggedIn = false
private var deviceUuid: String? = null
private var csrfToken: String? = null
private var userId: Long = 0
private var toolbarOwner: Fragment? = null
private lateinit var toolbar: Toolbar
var currentTabs: List = emptyList()
private set
private var showBottomViewDestinations: List = emptyList()
private val serviceConnection: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
// final ActivityCheckerService.LocalBinder binder = (ActivityCheckerService.LocalBinder) service;
// final ActivityCheckerService activityCheckerService = binder.getService();
isActivityCheckerServiceBound = true
}
override fun onServiceDisconnected(name: ComponentName) {
isActivityCheckerServiceBound = false
}
}
override fun onCreate(savedInstanceState: Bundle?) {
try {
DownloadUtils.init(
this,
Utils.settingsHelper.getString(PreferenceKeys.PREF_BARINSTA_DIR_URI)
)
} catch (e: ReselectDocumentTreeException) {
super.onCreate(savedInstanceState)
val intent = Intent(this, DirectorySelectActivity::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
intent.putExtra(EXTRA_INITIAL_URI, e.initialUri)
}
startActivity(intent)
finish()
return
}
super.onCreate(savedInstanceState)
instance = this
binding = ActivityMainBinding.inflate(layoutInflater)
toolbar = binding.toolbar
setupCookie()
if (Utils.settingsHelper.getBoolean(PreferenceKeys.FLAG_SECURE)) {
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
}
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
setupInsetsCallback()
createNotificationChannels()
val navHostFragment = supportFragmentManager.findFragmentById(R.id.main_nav_host) as NavHostFragment
navController = navHostFragment.navController
if (savedInstanceState == null) {
setupNavigation(true)
}
if (!BuildConfig.isPre) {
val checkUpdates = Utils.settingsHelper.getBoolean(PreferenceKeys.CHECK_UPDATES)
if (checkUpdates) FlavorTown.updateCheck(this)
}
FlavorTown.changelogCheck(this)
ViewModelProvider(this).get(AppStateViewModel::class.java) // Just initiate the App state here
handleIntent(intent)
if (isLoggedIn && Utils.settingsHelper.getBoolean(PreferenceKeys.CHECK_ACTIVITY)) {
bindActivityCheckerService()
}
// Initialise the internal map
tasksThread.execute {
EmojiParser.getInstance(this)
EmojiVariantManager.getInstance()
}
initEmojiCompat()
// initDmService();
initDmUnreadCount()
initSearchInput()
}
private fun setupInsetsCallback() {
val deferringInsetsCallback = RootViewDeferringInsetsCallback(
WindowInsetsCompat.Type.systemBars(),
WindowInsetsCompat.Type.ime()
)
ViewCompat.setWindowInsetsAnimationCallback(binding.root, deferringInsetsCallback)
ViewCompat.setOnApplyWindowInsetsListener(binding.root, deferringInsetsCallback)
WindowCompat.setDecorFitsSystemWindows(window, false)
}
private fun setupCookie() {
val cookie = Utils.settingsHelper.getString(Constants.COOKIE)
userId = 0
csrfToken = null
if (cookie.isNotBlank()) {
userId = getUserIdFromCookie(cookie)
csrfToken = getCsrfTokenFromCookie(cookie)
}
if (cookie.isBlank() || userId == 0L || csrfToken.isNullOrBlank()) {
isLoggedIn = false
return
}
deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID)
if (isEmpty(deviceUuid)) {
Utils.settingsHelper.putString(Constants.DEVICE_UUID, UUID.randomUUID().toString())
}
setupCookies(cookie)
isLoggedIn = true
}
@Suppress("unused")
private fun initDmService() {
if (!isLoggedIn) return
val enabled = Utils.settingsHelper.getBoolean(PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH)
if (!enabled) return
DMSyncAlarmReceiver.setAlarm(this)
}
private fun initDmUnreadCount() {
if (!isLoggedIn) return
val directInboxViewModel = ViewModelProvider(this).get(DirectInboxViewModel::class.java)
directInboxViewModel.unseenCount.observe(this, { unseenCountResource: Resource? ->
if (unseenCountResource == null) return@observe
val unseenCount = unseenCountResource.data
setNavBarDMUnreadCountBadge(unseenCount ?: 0)
})
}
private fun initSearchInput() {
binding.searchInputLayout.setEndIconOnClickListener {
val editText = binding.searchInputLayout.editText ?: return@setEndIconOnClickListener
editText.setText("")
}
binding.searchInputLayout.addOnEditTextAttachedListener { textInputLayout: TextInputLayout ->
textInputLayout.isEndIconVisible = false
val editText = textInputLayout.editText ?: return@addOnEditTextAttachedListener
editText.addTextChangedListener(object : TextWatcherAdapter() {
override fun afterTextChanged(s: Editable) {
binding.searchInputLayout.isEndIconVisible = !isEmpty(s)
}
})
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.main_menu, menu)
searchMenuItem = menu.findItem(R.id.search)
val currentDestination = navController.currentDestination
if (currentDestination != null) {
val backStack = navController.backQueue
setupMenu(backStack.size, currentDestination.id)
}
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.search) {
try {
navController.navigate(getSearchDeepLink())
return true
} catch (e: Exception) {
Log.e(TAG, "onOptionsItemSelected: ", e)
}
return false
}
return super.onOptionsItemSelected(item)
}
override fun onSaveInstanceState(outState: Bundle) {
// outState.putString(FIRST_FRAGMENT_GRAPH_INDEX_KEY, firstFragmentGraphIndex.toString())
outState.putString(LAST_SELECT_NAV_MENU_ID, binding.bottomNavView.selectedItemId.toString())
super.onSaveInstanceState(outState)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
val lastSelected = savedInstanceState[LAST_SELECT_NAV_MENU_ID] as String?
if (lastSelected != null) {
try {
lastSelectedNavMenuId = lastSelected.toInt()
} catch (ignored: NumberFormatException) {
}
}
setupNavigation(false)
}
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp(appBarConfiguration)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleIntent(intent)
}
override fun onDestroy() {
try {
super.onDestroy()
} catch (e: Exception) {
Log.e(TAG, "onDestroy: ", e)
}
unbindActivityCheckerService()
// try {
// RetrofitFactory.getInstance().destroy()
// } catch (e: Exception) {
// Log.e(TAG, "onDestroy: ", e)
// }
DownloadUtils.destroy()
instance = null
}
// override fun onBackPressed() {
// Log.d(TAG, "onBackPressed: ")
// navController.navigateUp()
// val backStack = navController.backQueue
// val currentNavControllerBackStack = backStack.size
// if (isTaskRoot && isBackStackEmpty && currentNavControllerBackStack == 2) {
// finishAfterTransition()
// return
// }
// if (!isFinishing) {
// try {
// super.onBackPressed()
// } catch (e: Exception) {
// Log.e(TAG, "onBackPressed: ", e)
// finish()
// }
// }
// }
private fun createNotificationChannels() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val notificationManager = NotificationManagerCompat.from(applicationContext)
notificationManager.createNotificationChannel(
NotificationChannel(
Constants.DOWNLOAD_CHANNEL_ID,
Constants.DOWNLOAD_CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT
)
)
notificationManager.createNotificationChannel(
NotificationChannel(
Constants.ACTIVITY_CHANNEL_ID,
Constants.ACTIVITY_CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT
)
)
notificationManager.createNotificationChannel(
NotificationChannel(
Constants.DM_UNREAD_CHANNEL_ID,
Constants.DM_UNREAD_CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT
)
)
val silentNotificationChannel = NotificationChannel(
Constants.SILENT_NOTIFICATIONS_CHANNEL_ID,
Constants.SILENT_NOTIFICATIONS_CHANNEL_NAME,
NotificationManager.IMPORTANCE_LOW
)
silentNotificationChannel.setSound(null, null)
notificationManager.createNotificationChannel(silentNotificationChannel)
}
private fun setupNavigation(setDefaultTabFromSettings: Boolean) {
currentTabs = if (isLoggedIn) setupMainBottomNav() else setupAnonBottomNav()
showBottomViewDestinations = currentTabs.asSequence().map {
it.startDestinationFragmentId
}.toMutableList().apply {
add(R.id.postViewFragment)
add(R.id.favorites_non_top)
add(R.id.notifications_viewer_non_top)
add(R.id.profile_non_top)
}
if (setDefaultTabFromSettings) {
setSelectedTab(currentTabs)
} else {
binding.bottomNavView.selectedItemId = lastSelectedNavMenuId
}
val navigatorProvider = navController.navigatorProvider
val navigator = navigatorProvider.getNavigator("navigation")
val rootNavGraph = NavGraph(navigator)
val navInflater = navController.navInflater
val topLevelDestinations = currentTabs.map { navInflater.inflate(it.navigationResId) }
rootNavGraph.id = R.id.root_nav_graph
rootNavGraph.label = "root_nav_graph"
rootNavGraph.addDestinations(topLevelDestinations)
rootNavGraph.setStartDestination(if (startNavRootId != 0) startNavRootId else R.id.profile_nav_graph)
navController.graph = rootNavGraph
binding.bottomNavView.setupWithNavController(navController)
appBarConfiguration = AppBarConfiguration(currentTabs.map { it.startDestinationFragmentId }.toSet())
setupActionBarWithNavController(navController, appBarConfiguration)
navController.addOnDestinationChangedListener { _: NavController?, destination: NavDestination, arguments: Bundle? ->
if (destination.id == R.id.directMessagesThreadFragment && arguments != null) {
// Set the thread title earlier for better ux
val title = arguments.getString("title")
if (!title.isNullOrBlank()) {
supportActionBar?.title = title
}
}
if (destination.id == R.id.profileFragment && arguments != null) {
// Set the title to username
val username = arguments.getString("username")
if (!username.isNullOrBlank()) {
supportActionBar?.title = username.substringAfter("@")
}
}
// below is a hack to check if we are at the end of the current stack, to setup the search view
binding.appBarLayout.setExpanded(true, true)
val destinationId = destination.id
val backStack = navController.backQueue
setupMenu(backStack.size, destinationId)
val contains = showBottomViewDestinations.contains(destinationId)
binding.root.post {
binding.bottomNavView.visibility = if (contains) View.VISIBLE else View.GONE
// if (contains) {
// behavior?.slideUp(binding.bottomNavView)
// }
}
// explicitly hide keyboard when we navigate
val view = currentFocus
Utils.hideKeyboard(view)
}
setupReselection()
}
private fun setupReselection() {
binding.bottomNavView.setOnItemReselectedListener {
val navHostFragment = (supportFragmentManager.primaryNavigationFragment ?: return@setOnItemReselectedListener) as NavHostFragment
val currentFragment = navHostFragment.childFragmentManager.fragments.firstOrNull() ?: return@setOnItemReselectedListener
if (currentFragment is FeedFragment) {
currentFragment.scrollToTop()
return@setOnItemReselectedListener
}
val currentDestination = navController.currentDestination ?: return@setOnItemReselectedListener
val currentTabStartDestId = (navController.getBackStackEntry(it.itemId).destination as NavGraph).startDestinationId
if (currentDestination.id == currentTabStartDestId) return@setOnItemReselectedListener
navController.popBackStack(currentTabStartDestId, false)
}
}
private fun setSelectedTab(tabs: List) {
val defaultTabResNameString = Utils.settingsHelper.getString(Constants.DEFAULT_TAB)
try {
var navId = 0
if (defaultTabResNameString.isNotBlank()) {
navId = resources.getIdentifier(defaultTabResNameString, "id", packageName)
}
val startFragmentNavResId = if (navId <= 0) R.id.profile_nav_graph else navId
val tab = tabs.firstOrNull { it.navigationRootId == startFragmentNavResId }
// if (index < 0 || index >= tabs.size) index = 0
val firstTab = tab ?: tabs[0]
startNavRootId = firstTab.navigationRootId
binding.bottomNavView.selectedItemId = firstTab.navigationRootId
} catch (e: Exception) {
Log.e(TAG, "Error parsing id", e)
}
}
private fun setupAnonBottomNav(): List {
val selectedItemId = binding.bottomNavView.selectedItemId
val anonNavTabs = getAnonNavTabs(this)
val menu = binding.bottomNavView.menu
menu.clear()
for (tab in anonNavTabs) {
menu.add(0, tab.navigationRootId, 0, tab.title).setIcon(tab.iconResId)
}
if (selectedItemId != R.id.profile_nav_graph && selectedItemId != R.id.more_nav_graph && selectedItemId != R.id.favorites_nav_graph) {
binding.bottomNavView.selectedItemId = R.id.profile_nav_graph
}
return anonNavTabs
}
private fun setupMainBottomNav(): List {
val menu = binding.bottomNavView.menu
menu.clear()
val navTabList = getLoggedInNavTabs(this).first
for (tab in navTabList) {
menu.add(0, tab.navigationRootId, 0, tab.title).setIcon(tab.iconResId)
}
return navTabList
}
private fun setupMenu(backStackSize: Int, destinationId: Int) {
val searchMenuItem = searchMenuItem ?: return
if (backStackSize >= 2 && SEARCH_VISIBLE_DESTINATIONS.contains(destinationId)) {
searchMenuItem.isVisible = true
return
}
searchMenuItem.isVisible = false
}
private fun setScrollingBehaviour() {
val layoutParams = binding.mainNavHost.layoutParams as CoordinatorLayout.LayoutParams
layoutParams.behavior = ScrollingViewBehavior()
binding.mainNavHost.requestLayout()
}
private fun removeScrollingBehaviour() {
val layoutParams = binding.mainNavHost.layoutParams as CoordinatorLayout.LayoutParams
layoutParams.behavior = null
binding.mainNavHost.requestLayout()
}
private fun handleIntent(intent: Intent?) {
if (intent == null) return
val action = intent.action
val type = intent.type
// Log.d(TAG, action + " " + type);
if (Intent.ACTION_MAIN == action) return
if (Constants.ACTION_SHOW_ACTIVITY == action) {
showActivityView()
return
}
if (Constants.ACTION_SHOW_DM_THREAD == action) {
showThread(intent)
return
}
if (Intent.ACTION_SEND == action && type != null) {
if (type == "text/plain") {
handleUrl(intent.getStringExtra(Intent.EXTRA_TEXT))
}
return
}
if (Intent.ACTION_VIEW == action) {
val data = intent.data ?: return
handleUrl(data.toString())
}
}
private fun showThread(intent: Intent) {
val threadId = intent.getStringExtra(Constants.DM_THREAD_ACTION_EXTRA_THREAD_ID)
val threadTitle = intent.getStringExtra(Constants.DM_THREAD_ACTION_EXTRA_THREAD_TITLE)
navigateToThread(threadId, threadTitle)
}
fun navigateToThread(threadId: String?, threadTitle: String?) {
if (threadId == null || threadTitle == null) return
try {
navController.navigate(getDirectThreadDeepLink(threadId, threadTitle))
} catch (e: Exception) {
Log.e(TAG, "navigateToThread: ", e)
}
}
private fun handleUrl(url: String?) {
if (url == null) return
// Log.d(TAG, url);
val intentModel = IntentUtils.parseUrl(url) ?: return
showView(intentModel)
}
private fun showView(intentModel: IntentModel) {
when (intentModel.type) {
IntentModelType.USERNAME -> showProfileView(intentModel)
IntentModelType.POST -> showPostView(intentModel)
IntentModelType.LOCATION -> showLocationView(intentModel)
IntentModelType.HASHTAG -> showHashtagView(intentModel)
IntentModelType.UNKNOWN -> Log.w(TAG, "Unknown model type received!")
// else -> Log.w(TAG, "Unknown model type received!")
}
}
private fun showProfileView(intentModel: IntentModel) {
try {
val username = intentModel.text
navController.navigate(getProfileDeepLink(username))
} catch (e: Exception) {
Log.e(TAG, "showProfileView: ", e)
}
}
private fun showPostView(intentModel: IntentModel) {
val shortCode = intentModel.text
// Log.d(TAG, "shortCode: " + shortCode);
try {
navController.navigate(getPostDeepLink(shortCode))
} catch (e: Exception) {
Log.e(TAG, "showPostView: ", e)
}
}
private fun showLocationView(intentModel: IntentModel) {
val locationId = intentModel.text
// Log.d(TAG, "locationId: " + locationId);
try {
navController.navigate(getLocationDeepLink(locationId))
} catch (e: Exception) {
Log.e(TAG, "showLocationView: ", e)
}
}
private fun showHashtagView(intentModel: IntentModel) {
val hashtag = intentModel.text
// Log.d(TAG, "hashtag: " + hashtag);
try {
navController.navigate(getHashtagDeepLink(hashtag))
} catch (e: Exception) {
Log.e(TAG, "showHashtagView: ", e)
}
}
private fun showActivityView() {
try {
navController.navigate(getNotificationsDeepLink("notif"))
} catch (e: Exception) {
Log.e(TAG, "showActivityView: ", e)
}
}
private fun bindActivityCheckerService() {
bindService(Intent(this, ActivityCheckerService::class.java), serviceConnection, BIND_AUTO_CREATE)
isActivityCheckerServiceBound = true
}
private fun unbindActivityCheckerService() {
if (!isActivityCheckerServiceBound) return
unbindService(serviceConnection)
isActivityCheckerServiceBound = false
}
val bottomNavView: BottomNavigationView
get() = binding.bottomNavView
// fun setCollapsingView(view: View) {
// try {
// binding.collapsingToolbarLayout.addView(view, 0)
// } catch (e: Exception) {
// Log.e(TAG, "setCollapsingView: ", e)
// }
// }
//
// fun removeCollapsingView(view: View) {
// try {
// binding.collapsingToolbarLayout.removeView(view)
// } catch (e: Exception) {
// Log.e(TAG, "removeCollapsingView: ", e)
// }
// }
val collapsingToolbarView: CollapsingToolbarLayout
get() = binding.collapsingToolbarLayout
val appbarLayout: AppBarLayout
get() = binding.appBarLayout
fun removeLayoutTransition() {
binding.root.layoutTransition = null
}
fun setLayoutTransition() {
binding.root.layoutTransition = LayoutTransition()
}
private fun initEmojiCompat() {
// Use a downloadable font for EmojiCompat
val fontRequest = FontRequest(
"com.google.android.gms.fonts",
"com.google.android.gms",
"Noto Color Emoji Compat",
R.array.com_google_android_gms_fonts_certs
)
val config: EmojiCompat.Config = FontRequestEmojiCompatConfig(applicationContext, fontRequest)
config.setReplaceAll(true) // .setUseEmojiAsDefaultStyle(true)
.registerInitCallback(object : InitCallback() {
override fun onInitialized() {
Log.i(TAG, "EmojiCompat initialized")
}
override fun onFailed(throwable: Throwable?) {
Log.e(TAG, "EmojiCompat initialization failed", throwable)
}
})
EmojiCompat.init(config)
}
val rootView: View
get() = binding.root
private val toolbarLock = Any()
fun getToolbar() = synchronized(toolbarLock) { this.toolbar }
fun setToolbar(toolbar: Toolbar, owner: Fragment) = synchronized(toolbarLock) {
supportActionBar?.subtitle = null
toolbarOwner = owner
binding.appBarLayout.visibility = View.GONE
removeScrollingBehaviour()
setSupportActionBar(toolbar)
this.toolbar = toolbar
NavigationUI.setupWithNavController(toolbar, navController, appBarConfiguration)
}
fun resetToolbar(owner: Fragment) = synchronized(toolbarLock) {
if (owner != toolbarOwner) return
this.toolbar = binding.toolbar
setSupportActionBar(binding.toolbar)
binding.appBarLayout.visibility = View.VISIBLE
setScrollingBehaviour()
setupActionBarWithNavController(navController, appBarConfiguration)
toolbarOwner = null
}
private fun setNavBarDMUnreadCountBadge(unseenCount: Int) {
val badge = binding.bottomNavView.getOrCreateBadge(R.id.direct_messages_nav_graph)
if (unseenCount == 0) {
badge.isVisible = false
badge.clearNumber()
return
}
if (badge.verticalOffset != 10) {
badge.verticalOffset = 10
}
badge.number = unseenCount
badge.isVisible = true
}
fun showSearchView(): TextInputLayout {
binding.searchInputLayout.visibility = View.VISIBLE
return binding.searchInputLayout
}
fun hideSearchView() {
binding.searchInputLayout.visibility = View.GONE
}
companion object {
private const val TAG = "MainActivity"
private const val LAST_SELECT_NAV_MENU_ID = "lastSelectedNavMenuId"
private val SEARCH_VISIBLE_DESTINATIONS: List = ImmutableList.of(
R.id.feedFragment,
R.id.profileFragment,
R.id.directMessagesInboxFragment,
R.id.discoverFragment,
R.id.favoritesFragment,
R.id.hashTagFragment,
R.id.locationFragment
)
@JvmStatic
var instance: MainActivity? = null
private set
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/AccountSwitcherAdapter.java
================================================
package awais.instagrabber.adapters;
import android.annotation.SuppressLint;
import android.graphics.Typeface;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.R;
import awais.instagrabber.databinding.PrefAccountSwitcherBinding;
import awais.instagrabber.db.entities.Account;
import awais.instagrabber.utils.Constants;
import static awais.instagrabber.utils.Utils.settingsHelper;
public class AccountSwitcherAdapter extends ListAdapter {
private static final String TAG = "AccountSwitcherAdapter";
private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() {
@Override
public boolean areItemsTheSame(@NonNull final Account oldItem, @NonNull final Account newItem) {
return oldItem.getUid().equals(newItem.getUid());
}
@Override
public boolean areContentsTheSame(@NonNull final Account oldItem, @NonNull final Account newItem) {
return oldItem.getUid().equals(newItem.getUid());
}
};
private final OnAccountClickListener clickListener;
private final OnAccountLongClickListener longClickListener;
public AccountSwitcherAdapter(final OnAccountClickListener clickListener,
final OnAccountLongClickListener longClickListener) {
super(DIFF_CALLBACK);
this.clickListener = clickListener;
this.longClickListener = longClickListener;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final PrefAccountSwitcherBinding binding = PrefAccountSwitcherBinding.inflate(layoutInflater, parent, false);
return new ViewHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull final ViewHolder holder, final int position) {
final Account model = getItem(position);
if (model == null) return;
final String cookie = settingsHelper.getString(Constants.COOKIE);
final boolean isCurrent = model.getCookie().equals(cookie);
holder.bind(model, isCurrent, clickListener, longClickListener);
}
public interface OnAccountClickListener {
void onAccountClick(final Account model, final boolean isCurrent);
}
public interface OnAccountLongClickListener {
boolean onAccountLongClick(final Account model, final boolean isCurrent);
}
public static class ViewHolder extends RecyclerView.ViewHolder {
private final PrefAccountSwitcherBinding binding;
public ViewHolder(final PrefAccountSwitcherBinding binding) {
super(binding.getRoot());
this.binding = binding;
binding.arrowDown.setImageResource(R.drawable.ic_check_24);
}
@SuppressLint("SetTextI18n")
public void bind(final Account model,
final boolean isCurrent,
final OnAccountClickListener clickListener,
final OnAccountLongClickListener longClickListener) {
// Log.d(TAG, model.getFullName());
itemView.setOnClickListener(v -> {
if (clickListener == null) return;
clickListener.onAccountClick(model, isCurrent);
});
itemView.setOnLongClickListener(v -> {
if (longClickListener == null) return false;
return longClickListener.onAccountLongClick(model, isCurrent);
});
binding.profilePic.setImageURI(model.getProfilePic());
binding.username.setText("@" + model.getUsername());
binding.fullName.setTypeface(null);
final String fullName = model.getFullName();
if (TextUtils.isEmpty(fullName)) {
binding.fullName.setVisibility(View.GONE);
} else {
binding.fullName.setVisibility(View.VISIBLE);
binding.fullName.setText(fullName);
}
if (!isCurrent) {
binding.arrowDown.setVisibility(View.GONE);
return;
}
binding.fullName.setTypeface(binding.fullName.getTypeface(), Typeface.BOLD);
binding.arrowDown.setVisibility(View.VISIBLE);
}
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/CommentsAdapter.java
================================================
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import java.util.Objects;
import awais.instagrabber.adapters.viewholder.CommentViewHolder;
import awais.instagrabber.databinding.ItemCommentBinding;
import awais.instagrabber.models.Comment;
public final class CommentsAdapter extends ListAdapter {
private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() {
@Override
public boolean areItemsTheSame(@NonNull final Comment oldItem, @NonNull final Comment newItem) {
return Objects.equals(oldItem.getPk(), newItem.getPk());
}
@Override
public boolean areContentsTheSame(@NonNull final Comment oldItem, @NonNull final Comment newItem) {
return Objects.equals(oldItem, newItem);
}
};
private final boolean showingReplies;
private final CommentCallback commentCallback;
private final long currentUserId;
public CommentsAdapter(final long currentUserId,
final boolean showingReplies,
final CommentCallback commentCallback) {
super(DIFF_CALLBACK);
this.showingReplies = showingReplies;
this.currentUserId = currentUserId;
this.commentCallback = commentCallback;
}
@NonNull
@Override
public CommentViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int type) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final ItemCommentBinding binding = ItemCommentBinding.inflate(layoutInflater, parent, false);
return new CommentViewHolder(binding, currentUserId, commentCallback);
}
@Override
public void onBindViewHolder(@NonNull final CommentViewHolder holder, final int position) {
final Comment comment = getItem(position);
holder.bind(comment, showingReplies && position == 0, showingReplies && position != 0);
}
public interface CommentCallback {
void onClick(final Comment comment);
void onHashtagClick(final String hashtag);
void onMentionClick(final String mention);
void onURLClick(final String url);
void onEmailClick(final String emailAddress);
void onLikeClick(final Comment comment, boolean liked, final boolean isReply);
void onRepliesClick(final Comment comment);
void onViewLikes(Comment comment);
void onTranslate(Comment comment);
void onDelete(Comment comment, boolean isReply);
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/DirectItemsAdapter.java
================================================
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.AdapterListUpdateCallback;
import androidx.recyclerview.widget.AsyncDifferConfig;
import androidx.recyclerview.widget.AsyncListDiffer;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemActionLogViewHolder;
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemAnimatedMediaViewHolder;
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemDefaultViewHolder;
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemLikeViewHolder;
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemLinkViewHolder;
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemMediaShareViewHolder;
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemMediaViewHolder;
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemPlaceholderViewHolder;
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemProfileViewHolder;
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemRavenMediaViewHolder;
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemReelShareViewHolder;
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemStoryShareViewHolder;
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemTextViewHolder;
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemVideoCallEventViewHolder;
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemViewHolder;
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemVoiceMediaViewHolder;
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemXmaViewHolder;
import awais.instagrabber.customviews.emoji.Emoji;
import awais.instagrabber.databinding.LayoutDmActionLogBinding;
import awais.instagrabber.databinding.LayoutDmAnimatedMediaBinding;
import awais.instagrabber.databinding.LayoutDmBaseBinding;
import awais.instagrabber.databinding.LayoutDmHeaderBinding;
import awais.instagrabber.databinding.LayoutDmLikeBinding;
import awais.instagrabber.databinding.LayoutDmLinkBinding;
import awais.instagrabber.databinding.LayoutDmMediaBinding;
import awais.instagrabber.databinding.LayoutDmMediaShareBinding;
import awais.instagrabber.databinding.LayoutDmProfileBinding;
import awais.instagrabber.databinding.LayoutDmRavenMediaBinding;
import awais.instagrabber.databinding.LayoutDmReelShareBinding;
import awais.instagrabber.databinding.LayoutDmStoryShareBinding;
import awais.instagrabber.databinding.LayoutDmTextBinding;
import awais.instagrabber.databinding.LayoutDmVoiceMediaBinding;
import awais.instagrabber.models.enums.DirectItemType;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectItemStoryShare;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
public final class DirectItemsAdapter extends RecyclerView.Adapter {
private static final String TAG = DirectItemsAdapter.class.getSimpleName();
private List items;
private DirectThread thread;
private DirectItemViewHolder selectedViewHolder;
private final User currentUser;
private final DirectItemCallback callback;
private final AsyncListDiffer differ;
private final DirectItemInternalLongClickListener longClickListener;
private static final DiffUtil.ItemCallback diffCallback = new DiffUtil.ItemCallback() {
@Override
public boolean areItemsTheSame(@NonNull final DirectItemOrHeader oldItem, @NonNull final DirectItemOrHeader newItem) {
final boolean bothHeaders = oldItem.isHeader() && newItem.isHeader();
final boolean bothItems = !oldItem.isHeader() && !newItem.isHeader();
boolean areSameType = bothHeaders || bothItems;
if (!areSameType) return false;
if (bothHeaders) {
return oldItem.date.equals(newItem.date);
}
if (oldItem.item != null && newItem.item != null) {
String oldClientContext = oldItem.item.getClientContext();
if (oldClientContext == null) {
oldClientContext = oldItem.item.getItemId();
}
String newClientContext = newItem.item.getClientContext();
if (newClientContext == null) {
newClientContext = newItem.item.getItemId();
}
return oldClientContext.equals(newClientContext);
}
return false;
}
@Override
public boolean areContentsTheSame(@NonNull final DirectItemOrHeader oldItem, @NonNull final DirectItemOrHeader newItem) {
final boolean bothHeaders = oldItem.isHeader() && newItem.isHeader();
final boolean bothItems = !oldItem.isHeader() && !newItem.isHeader();
boolean areSameType = bothHeaders || bothItems;
if (!areSameType) return false;
if (bothHeaders) {
return oldItem.date.equals(newItem.date);
}
final boolean timestampEqual = oldItem.item.getTimestamp() == newItem.item.getTimestamp();
final boolean bothPending = oldItem.item.isPending() == newItem.item.isPending();
final boolean reactionSame = Objects.equals(oldItem.item.getReactions(), newItem.item.getReactions());
return timestampEqual && bothPending && reactionSame;
}
};
public DirectItemsAdapter(@NonNull final User currentUser,
@NonNull final DirectThread thread,
@NonNull final DirectItemCallback callback,
@NonNull final DirectItemLongClickListener itemLongClickListener) {
this.currentUser = currentUser;
this.thread = thread;
this.callback = callback;
differ = new AsyncListDiffer<>(new AdapterListUpdateCallback(this),
new AsyncDifferConfig.Builder<>(diffCallback).build());
longClickListener = (position, viewHolder) -> {
if (selectedViewHolder != null) {
selectedViewHolder.setSelected(false);
}
selectedViewHolder = viewHolder;
viewHolder.setSelected(true);
itemLongClickListener.onLongClick(position);
};
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int type) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
if (type == -1) {
// header
return new HeaderViewHolder(LayoutDmHeaderBinding.inflate(layoutInflater, parent, false));
}
final LayoutDmBaseBinding baseBinding = LayoutDmBaseBinding.inflate(layoutInflater, parent, false);
final DirectItemType directItemType = DirectItemType.Companion.getTypeFromId(type);
final DirectItemViewHolder itemViewHolder = getItemViewHolder(layoutInflater, baseBinding, directItemType);
itemViewHolder.setLongClickListener(longClickListener);
return itemViewHolder;
}
@NonNull
private DirectItemViewHolder getItemViewHolder(final LayoutInflater layoutInflater,
final LayoutDmBaseBinding baseBinding,
@NonNull final DirectItemType directItemType) {
switch (directItemType) {
case TEXT: {
final LayoutDmTextBinding binding = LayoutDmTextBinding.inflate(layoutInflater, baseBinding.message, false);
return new DirectItemTextViewHolder(baseBinding, binding, currentUser, thread, callback);
}
case LIKE: {
final LayoutDmLikeBinding binding = LayoutDmLikeBinding.inflate(layoutInflater, baseBinding.message, false);
return new DirectItemLikeViewHolder(baseBinding, binding, currentUser, thread, callback);
}
case LINK: {
final LayoutDmLinkBinding binding = LayoutDmLinkBinding.inflate(layoutInflater, baseBinding.message, false);
return new DirectItemLinkViewHolder(baseBinding, binding, currentUser, thread, callback);
}
case ACTION_LOG: {
final LayoutDmActionLogBinding binding = LayoutDmActionLogBinding.inflate(layoutInflater, baseBinding.message, false);
return new DirectItemActionLogViewHolder(baseBinding, binding, currentUser, thread, callback);
}
case VIDEO_CALL_EVENT: {
final LayoutDmActionLogBinding binding = LayoutDmActionLogBinding.inflate(layoutInflater, baseBinding.message, false);
return new DirectItemVideoCallEventViewHolder(baseBinding, binding, currentUser, thread, callback);
}
case PLACEHOLDER: {
final LayoutDmStoryShareBinding binding = LayoutDmStoryShareBinding.inflate(layoutInflater, baseBinding.message, false);
return new DirectItemPlaceholderViewHolder(baseBinding, binding, currentUser, thread, callback);
}
case ANIMATED_MEDIA: {
final LayoutDmAnimatedMediaBinding binding = LayoutDmAnimatedMediaBinding.inflate(layoutInflater, baseBinding.message, false);
return new DirectItemAnimatedMediaViewHolder(baseBinding, binding, currentUser, thread, callback);
}
case VOICE_MEDIA: {
final LayoutDmVoiceMediaBinding binding = LayoutDmVoiceMediaBinding.inflate(layoutInflater, baseBinding.message, false);
return new DirectItemVoiceMediaViewHolder(baseBinding, binding, currentUser, thread, callback);
}
case LOCATION:
case PROFILE: {
final LayoutDmProfileBinding binding = LayoutDmProfileBinding.inflate(layoutInflater, baseBinding.message, false);
return new DirectItemProfileViewHolder(baseBinding, binding, currentUser, thread, callback);
}
case MEDIA: {
final LayoutDmMediaBinding binding = LayoutDmMediaBinding.inflate(layoutInflater, baseBinding.message, false);
return new DirectItemMediaViewHolder(baseBinding, binding, currentUser, thread, callback);
}
case CLIP:
case FELIX_SHARE:
case MEDIA_SHARE: {
final LayoutDmMediaShareBinding binding = LayoutDmMediaShareBinding.inflate(layoutInflater, baseBinding.message, false);
return new DirectItemMediaShareViewHolder(baseBinding, binding, currentUser, thread, callback);
}
case STORY_SHARE: {
final LayoutDmStoryShareBinding binding = LayoutDmStoryShareBinding.inflate(layoutInflater, baseBinding.message, false);
return new DirectItemStoryShareViewHolder(baseBinding, binding, currentUser, thread, callback);
}
case REEL_SHARE: {
final LayoutDmReelShareBinding binding = LayoutDmReelShareBinding.inflate(layoutInflater, baseBinding.message, false);
return new DirectItemReelShareViewHolder(baseBinding, binding, currentUser, thread, callback);
}
case RAVEN_MEDIA: {
final LayoutDmRavenMediaBinding binding = LayoutDmRavenMediaBinding.inflate(layoutInflater, baseBinding.message, false);
return new DirectItemRavenMediaViewHolder(baseBinding, binding, currentUser, thread, callback);
}
case XMA: {
final LayoutDmAnimatedMediaBinding binding = LayoutDmAnimatedMediaBinding.inflate(layoutInflater, baseBinding.message, false);
return new DirectItemXmaViewHolder(baseBinding, binding, currentUser, thread, callback);
}
case UNKNOWN:
default: {
final LayoutDmTextBinding binding = LayoutDmTextBinding.inflate(layoutInflater, baseBinding.message, false);
return new DirectItemDefaultViewHolder(baseBinding, binding, currentUser, thread, callback);
}
}
}
@Override
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position) {
final DirectItemOrHeader itemOrHeader = getItem(position);
if (itemOrHeader.isHeader()) {
((HeaderViewHolder) holder).bind(itemOrHeader.date);
return;
}
if (thread == null) return;
((DirectItemViewHolder) holder).bind(position, itemOrHeader.item);
}
protected DirectItemOrHeader getItem(int position) {
return differ.getCurrentList().get(position);
}
@Override
public int getItemCount() {
return differ.getCurrentList().size();
}
@Override
public int getItemViewType(final int position) {
final DirectItemOrHeader itemOrHeader = getItem(position);
if (itemOrHeader.isHeader()) {
return -1;
}
final DirectItemType itemType = itemOrHeader.item.getItemType();
if (itemType == null) {
return 0;
}
return itemType.getId();
}
@Override
public long getItemId(final int position) {
final DirectItemOrHeader itemOrHeader = getItem(position);
if (itemOrHeader.isHeader()) {
return itemOrHeader.date.hashCode();
}
if (itemOrHeader.item.getClientContext() == null) {
return itemOrHeader.item.getItemId().hashCode();
}
return itemOrHeader.item.getClientContext().hashCode();
}
public void setThread(final DirectThread thread) {
if (thread == null) return;
this.thread = thread;
// notifyDataSetChanged();
}
public void submitList(@Nullable final List list) {
if (list == null) {
differ.submitList(null);
return;
}
differ.submitList(sectionAndSort(list));
this.items = list;
}
public void submitList(@Nullable final List list, @Nullable final Runnable commitCallback) {
if (list == null) {
differ.submitList(null, commitCallback);
return;
}
differ.submitList(sectionAndSort(list), commitCallback);
this.items = list;
}
private List sectionAndSort(final List list) {
final List itemOrHeaders = new ArrayList<>();
LocalDate prevSectionDate = null;
for (int i = 0; i < list.size(); i++) {
final DirectItem item = list.get(i);
if (item == null || item.getDate() == null) continue;
final DirectItemOrHeader prev = itemOrHeaders.isEmpty() ? null : itemOrHeaders.get(itemOrHeaders.size() - 1);
if (prev != null
&& prev.item != null
&& prev.item.getDate() != null
&& prev.item.getDate().toLocalDate().isEqual(item.getDate().toLocalDate())) {
// just add item
final DirectItemOrHeader itemOrHeader = new DirectItemOrHeader();
itemOrHeader.item = item;
itemOrHeaders.add(itemOrHeader);
if (i == list.size() - 1) {
// add header
final DirectItemOrHeader itemOrHeader2 = new DirectItemOrHeader();
itemOrHeader2.date = prevSectionDate;
itemOrHeaders.add(itemOrHeader2);
}
continue;
}
if (prevSectionDate != null) {
// add header
final DirectItemOrHeader itemOrHeader = new DirectItemOrHeader();
itemOrHeader.date = prevSectionDate;
itemOrHeaders.add(itemOrHeader);
}
// Add item
final DirectItemOrHeader itemOrHeader = new DirectItemOrHeader();
itemOrHeader.item = item;
itemOrHeaders.add(itemOrHeader);
prevSectionDate = item.getDate().toLocalDate();
}
return itemOrHeaders;
}
public List getList() {
return differ.getCurrentList();
}
public List getItems() {
return items;
}
@Override
public void onViewRecycled(@NonNull final RecyclerView.ViewHolder holder) {
if (holder instanceof DirectItemViewHolder) {
((DirectItemViewHolder) holder).cleanup();
}
}
@Override
public void onViewDetachedFromWindow(@NonNull final RecyclerView.ViewHolder holder) {
if (holder instanceof DirectItemViewHolder) {
((DirectItemViewHolder) holder).cleanup();
}
}
public DirectThread getThread() {
return thread;
}
public static class DirectItemOrHeader {
LocalDate date;
public DirectItem item;
public boolean isHeader() {
return date != null;
}
@NonNull
@Override
public String toString() {
return "DirectItemOrHeader{" +
"date=" + date +
", item=" + (item != null ? item.getItemType() : null) +
'}';
}
}
public static class HeaderViewHolder extends RecyclerView.ViewHolder {
private final LayoutDmHeaderBinding binding;
public HeaderViewHolder(@NonNull final LayoutDmHeaderBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
public void bind(final LocalDate date) {
if (date == null) {
binding.header.setText("");
return;
}
final DateTimeFormatter dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT);
binding.header.setText(dateFormatter.format(date));
}
}
public interface DirectItemCallback {
void onHashtagClick(String hashtag);
void onMentionClick(String mention);
void onLocationClick(long locationId);
void onURLClick(String url);
void onEmailClick(String email);
void onMediaClick(Media media, int index);
void onStoryClick(DirectItemStoryShare storyShare);
void onReaction(DirectItem item, Emoji emoji);
void onReactionClick(DirectItem item, int position);
void onOptionSelect(DirectItem item, @IdRes int itemId, final Function callback);
void onAddReactionListener(DirectItem item);
}
public interface DirectItemInternalLongClickListener {
void onLongClick(int position, DirectItemViewHolder viewHolder);
}
public interface DirectItemLongClickListener {
void onLongClick(int position);
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/DirectMessageInboxAdapter.java
================================================
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.AsyncDifferConfig;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import java.util.List;
import java.util.Objects;
import awais.instagrabber.adapters.viewholder.directmessages.DirectInboxItemViewHolder;
import awais.instagrabber.databinding.LayoutDmInboxItemBinding;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
public final class DirectMessageInboxAdapter extends ListAdapter {
private final OnItemClickListener onClickListener;
private static final DiffUtil.ItemCallback diffCallback = new DiffUtil.ItemCallback() {
@Override
public boolean areItemsTheSame(@NonNull final DirectThread oldItem, @NonNull final DirectThread newItem) {
return oldItem.getThreadId().equals(newItem.getThreadId());
}
@Override
public boolean areContentsTheSame(@NonNull final DirectThread oldThread,
@NonNull final DirectThread newThread) {
final boolean titleEqual = oldThread.getThreadTitle().equals(newThread.getThreadTitle());
if (!titleEqual) return false;
final boolean lastSeenAtEqual = Objects.equals(oldThread.getLastSeenAt(), newThread.getLastSeenAt());
if (!lastSeenAtEqual) return false;
final List oldItems = oldThread.getItems();
final List newItems = newThread.getItems();
if (oldItems == null || newItems == null) return false;
if (oldItems.size() != newItems.size()) return false;
final DirectItem oldItemFirst = oldThread.getFirstDirectItem();
final DirectItem newItemFirst = newThread.getFirstDirectItem();
if (oldItemFirst == null || newItemFirst == null) return false;
final boolean idsEqual = oldItemFirst.getItemId().equals(newItemFirst.getItemId());
if (!idsEqual) return false;
return oldItemFirst.getTimestamp() == newItemFirst.getTimestamp();
}
};
public DirectMessageInboxAdapter(final OnItemClickListener onClickListener) {
super(new AsyncDifferConfig.Builder<>(diffCallback).build());
this.onClickListener = onClickListener;
}
@NonNull
@Override
public DirectInboxItemViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int type) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final LayoutDmInboxItemBinding binding = LayoutDmInboxItemBinding.inflate(layoutInflater, parent, false);
return new DirectInboxItemViewHolder(binding, onClickListener);
}
@Override
public void onBindViewHolder(@NonNull final DirectInboxItemViewHolder holder, final int position) {
final DirectThread thread = getItem(position);
holder.bind(thread);
}
@Override
public long getItemId(final int position) {
return getItem(position).getThreadId().hashCode();
}
public interface OnItemClickListener {
void onItemClick(final DirectThread thread);
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/DirectPendingUsersAdapter.java
================================================
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import awais.instagrabber.adapters.viewholder.directmessages.DirectPendingUserViewHolder;
import awais.instagrabber.databinding.LayoutDmPendingUserItemBinding;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectThreadParticipantRequestsResponse;
public final class DirectPendingUsersAdapter extends ListAdapter {
private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() {
@Override
public boolean areItemsTheSame(@NonNull final PendingUser oldItem, @NonNull final PendingUser newItem) {
return oldItem.user.getPk() == newItem.user.getPk();
}
@Override
public boolean areContentsTheSame(@NonNull final PendingUser oldItem, @NonNull final PendingUser newItem) {
return Objects.equals(oldItem.user.getUsername(), newItem.user.getUsername()) &&
Objects.equals(oldItem.user.getFullName(), newItem.user.getFullName()) &&
Objects.equals(oldItem.requester, newItem.requester);
}
};
private final PendingUserCallback callback;
public DirectPendingUsersAdapter(final PendingUserCallback callback) {
super(DIFF_CALLBACK);
this.callback = callback;
setHasStableIds(true);
}
public void submitPendingRequests(final DirectThreadParticipantRequestsResponse requests) {
if (requests == null || requests.getUsers() == null) {
submitList(Collections.emptyList());
return;
}
submitList(parse(requests));
}
private List parse(final DirectThreadParticipantRequestsResponse requests) {
final List users = requests.getUsers();
final Map requesterUsernames = requests.getRequesterUsernames();
return users.stream()
.map(user -> new PendingUser(user, requesterUsernames.get(user.getPk())))
.collect(Collectors.toList());
}
@NonNull
@Override
public DirectPendingUserViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final LayoutDmPendingUserItemBinding binding = LayoutDmPendingUserItemBinding.inflate(layoutInflater, parent, false);
return new DirectPendingUserViewHolder(binding, callback);
}
@Override
public void onBindViewHolder(@NonNull final DirectPendingUserViewHolder holder, final int position) {
final PendingUser pendingUser = getItem(position);
holder.bind(position, pendingUser);
}
@Override
public long getItemId(final int position) {
final PendingUser item = getItem(position);
return item.user.getPk();
}
public static class PendingUser {
private final User user;
private final String requester;
private boolean inProgress;
public PendingUser(final User user, final String requester) {
this.user = user;
this.requester = requester;
}
public User getUser() {
return user;
}
public String getRequester() {
return requester;
}
public boolean isInProgress() {
return inProgress;
}
public PendingUser setInProgress(final boolean inProgress) {
this.inProgress = inProgress;
return this;
}
}
public interface PendingUserCallback {
void onClick(int position, PendingUser pendingUser);
void onApprove(int position, PendingUser pendingUser);
void onDeny(int position, PendingUser pendingUser);
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/DirectReactionsAdapter.java
================================================
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import java.util.List;
import awais.instagrabber.adapters.viewholder.directmessages.DirectReactionViewHolder;
import awais.instagrabber.databinding.LayoutDmUserItemBinding;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItemEmojiReaction;
public final class DirectReactionsAdapter extends ListAdapter {
private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() {
@Override
public boolean areItemsTheSame(@NonNull final DirectItemEmojiReaction oldItem, @NonNull final DirectItemEmojiReaction newItem) {
return oldItem.getSenderId() == newItem.getSenderId();
}
@Override
public boolean areContentsTheSame(@NonNull final DirectItemEmojiReaction oldItem, @NonNull final DirectItemEmojiReaction newItem) {
return oldItem.getEmoji().equals(newItem.getEmoji());
}
};
private final long viewerId;
private final List users;
private final String itemId;
private final OnReactionClickListener onReactionClickListener;
public DirectReactionsAdapter(final long viewerId,
final List users,
final String itemId,
final OnReactionClickListener onReactionClickListener) {
super(DIFF_CALLBACK);
this.viewerId = viewerId;
this.users = users;
this.itemId = itemId;
this.onReactionClickListener = onReactionClickListener;
setHasStableIds(true);
}
@NonNull
@Override
public DirectReactionViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final LayoutDmUserItemBinding binding = LayoutDmUserItemBinding.inflate(layoutInflater, parent, false);
return new DirectReactionViewHolder(binding, viewerId, itemId, onReactionClickListener);
}
@Override
public void onBindViewHolder(@NonNull final DirectReactionViewHolder holder, final int position) {
final DirectItemEmojiReaction reaction = getItem(position);
if (reaction == null) return;
holder.bind(reaction, getUser(reaction.getSenderId()));
}
@Override
public long getItemId(final int position) {
return getItem(position).getSenderId();
}
@Nullable
private User getUser(final long pk) {
return users.stream()
.filter(user -> user.getPk() == pk)
.findFirst()
.orElse(null);
}
public interface OnReactionClickListener {
void onReactionClick(String itemId, DirectItemEmojiReaction reaction);
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/DirectUsersAdapter.java
================================================
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import com.google.common.collect.ImmutableList;
import java.util.List;
import awais.instagrabber.R;
import awais.instagrabber.adapters.viewholder.directmessages.DirectUserViewHolder;
import awais.instagrabber.databinding.ItemFavSectionHeaderBinding;
import awais.instagrabber.databinding.LayoutDmUserItemBinding;
import awais.instagrabber.repositories.responses.User;
public final class DirectUsersAdapter extends ListAdapter {
private static final int VIEW_TYPE_HEADER = 0;
private static final int VIEW_TYPE_USER = 1;
private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() {
@Override
public boolean areItemsTheSame(@NonNull final DirectUserOrHeader oldItem, @NonNull final DirectUserOrHeader newItem) {
final boolean bothHeaders = oldItem.isHeader() && newItem.isHeader();
final boolean bothItems = !oldItem.isHeader() && !newItem.isHeader();
boolean areSameType = bothHeaders || bothItems;
if (!areSameType) return false;
if (bothHeaders) {
return oldItem.headerTitle == newItem.headerTitle;
}
if (oldItem.user != null && newItem.user != null) {
return oldItem.user.getPk() == newItem.user.getPk();
}
return false;
}
@Override
public boolean areContentsTheSame(@NonNull final DirectUserOrHeader oldItem, @NonNull final DirectUserOrHeader newItem) {
final boolean bothHeaders = oldItem.isHeader() && newItem.isHeader();
final boolean bothItems = !oldItem.isHeader() && !newItem.isHeader();
boolean areSameType = bothHeaders || bothItems;
if (!areSameType) return false;
if (bothHeaders) {
return oldItem.headerTitle == newItem.headerTitle;
}
if (oldItem.user != null && newItem.user != null) {
return oldItem.user.getUsername().equals(newItem.user.getUsername()) &&
oldItem.user.getFullName().equals(newItem.user.getFullName());
}
return false;
}
};
private final long inviterId;
private final OnDirectUserClickListener onClickListener;
private final OnDirectUserLongClickListener onLongClickListener;
private List adminUserIds;
public DirectUsersAdapter(final long inviterId,
final OnDirectUserClickListener onClickListener,
final OnDirectUserLongClickListener onLongClickListener) {
super(DIFF_CALLBACK);
this.inviterId = inviterId;
this.onClickListener = onClickListener;
this.onLongClickListener = onLongClickListener;
setHasStableIds(true);
}
public void submitUsers(final List users, final List leftUsers) {
if (users == null && leftUsers == null) return;
final List userOrHeaders = combineLists(users, leftUsers);
submitList(userOrHeaders);
}
private List combineLists(final List users, final List leftUsers) {
final ImmutableList.Builder listBuilder = ImmutableList.builder();
if (users != null && !users.isEmpty()) {
listBuilder.add(new DirectUserOrHeader(R.string.members));
users.stream()
.map(DirectUserOrHeader::new)
.forEach(listBuilder::add);
}
if (leftUsers != null && !leftUsers.isEmpty()) {
listBuilder.add(new DirectUserOrHeader(R.string.dms_left_users));
leftUsers.stream()
.map(DirectUserOrHeader::new)
.forEach(listBuilder::add);
}
return listBuilder.build();
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
switch (viewType) {
case VIEW_TYPE_USER:
final LayoutDmUserItemBinding binding = LayoutDmUserItemBinding.inflate(layoutInflater, parent, false);
return new DirectUserViewHolder(binding, onClickListener, onLongClickListener);
case VIEW_TYPE_HEADER:
default:
final ItemFavSectionHeaderBinding headerBinding = ItemFavSectionHeaderBinding.inflate(layoutInflater, parent, false);
return new HeaderViewHolder(headerBinding);
}
}
@Override
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position) {
if (holder instanceof HeaderViewHolder) {
((HeaderViewHolder) holder).bind(getItem(position).headerTitle);
return;
}
if (holder instanceof DirectUserViewHolder) {
final User user = getItem(position).user;
((DirectUserViewHolder) holder).bind(position,
user,
user != null && adminUserIds != null && adminUserIds.contains(user.getPk()),
user != null && user.getPk() == inviterId,
false,
false);
}
}
@Override
public int getItemViewType(final int position) {
final DirectUserOrHeader item = getItem(position);
return item.isHeader() ? VIEW_TYPE_HEADER : VIEW_TYPE_USER;
}
@Override
public long getItemId(final int position) {
final DirectUserOrHeader item = getItem(position);
return item.isHeader() ? item.headerTitle : item.user.getPk();
}
public void setAdminUserIds(final List adminUserIds) {
this.adminUserIds = adminUserIds;
notifyDataSetChanged();
}
public static class DirectUserOrHeader {
int headerTitle;
User user;
public DirectUserOrHeader(final int headerTitle) {
this.headerTitle = headerTitle;
}
public DirectUserOrHeader(final User user) {
this.user = user;
}
boolean isHeader() {
return headerTitle > 0;
}
}
public static class HeaderViewHolder extends RecyclerView.ViewHolder {
private final ItemFavSectionHeaderBinding binding;
public HeaderViewHolder(@NonNull final ItemFavSectionHeaderBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
public void bind(@StringRes final int headerTitle) {
binding.getRoot().setText(headerTitle);
}
}
public interface OnDirectUserClickListener {
void onClick(int position, User user, boolean selected);
}
public interface OnDirectUserLongClickListener {
boolean onLongClick(int position, User user);
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/DirectoryFilesAdapter.java
================================================
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import java.io.File;
import awais.instagrabber.R;
import awais.instagrabber.databinding.ItemDirListBinding;
public final class DirectoryFilesAdapter extends ListAdapter {
private final OnFileClickListener onFileClickListener;
private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() {
@Override
public boolean areItemsTheSame(@NonNull final File oldItem, @NonNull final File newItem) {
return oldItem.getAbsolutePath().equals(newItem.getAbsolutePath());
}
@Override
public boolean areContentsTheSame(@NonNull final File oldItem, @NonNull final File newItem) {
return oldItem.getAbsolutePath().equals(newItem.getAbsolutePath());
}
};
public DirectoryFilesAdapter(final OnFileClickListener onFileClickListener) {
super(DIFF_CALLBACK);
this.onFileClickListener = onFileClickListener;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
final ItemDirListBinding binding = ItemDirListBinding.inflate(inflater, parent, false);
return new ViewHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull final ViewHolder holder, final int position) {
final File file = getItem(position);
holder.bind(file, onFileClickListener);
}
public interface OnFileClickListener {
void onFileClick(File file);
}
static final class ViewHolder extends RecyclerView.ViewHolder {
private final ItemDirListBinding binding;
private ViewHolder(final ItemDirListBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
public void bind(final File file, final OnFileClickListener onFileClickListener) {
if (file == null) return;
if (onFileClickListener != null) {
itemView.setOnClickListener(v -> onFileClickListener.onFileClick(file));
}
binding.text.setText(file.getName());
if (file.isDirectory()) {
binding.icon.setImageResource(R.drawable.ic_folder_24);
return;
}
binding.icon.setImageResource(R.drawable.ic_file_24);
}
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/DiscoverTopicsAdapter.java
================================================
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import awais.instagrabber.adapters.viewholder.TopicClusterViewHolder;
import awais.instagrabber.databinding.ItemDiscoverTopicBinding;
import awais.instagrabber.repositories.responses.discover.TopicCluster;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.utils.ResponseBodyUtils;
public class DiscoverTopicsAdapter extends ListAdapter {
private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() {
@Override
public boolean areItemsTheSame(@NonNull final TopicCluster oldItem, @NonNull final TopicCluster newItem) {
return oldItem.getId().equals(newItem.getId());
}
@Override
public boolean areContentsTheSame(@NonNull final TopicCluster oldItem, @NonNull final TopicCluster newItem) {
final String oldThumbUrl = ResponseBodyUtils.getThumbUrl(oldItem.getCoverMedia());
return oldThumbUrl != null && oldThumbUrl.equals(ResponseBodyUtils.getThumbUrl(newItem.getCoverMedia()))
&& oldItem.getTitle().equals(newItem.getTitle());
}
};
private final OnTopicClickListener onTopicClickListener;
public DiscoverTopicsAdapter(final OnTopicClickListener onTopicClickListener) {
super(DIFF_CALLBACK);
this.onTopicClickListener = onTopicClickListener;
}
@NonNull
@Override
public TopicClusterViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final ItemDiscoverTopicBinding binding = ItemDiscoverTopicBinding.inflate(layoutInflater, parent, false);
return new TopicClusterViewHolder(binding, onTopicClickListener, null);
}
@Override
public void onBindViewHolder(@NonNull final TopicClusterViewHolder holder, final int position) {
final TopicCluster topicCluster = getItem(position);
holder.bind(topicCluster);
}
public interface OnTopicClickListener {
void onTopicClick(TopicCluster topicCluster, View cover, int titleColor, int backgroundColor);
void onTopicLongClick(Media coverMedia);
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/FavoritesAdapter.java
================================================
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.ObjectsCompat;
import androidx.recyclerview.widget.AdapterListUpdateCallback;
import androidx.recyclerview.widget.AsyncDifferConfig;
import androidx.recyclerview.widget.AsyncListDiffer;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import awais.instagrabber.R;
import awais.instagrabber.adapters.viewholder.FavoriteViewHolder;
import awais.instagrabber.databinding.ItemFavSectionHeaderBinding;
import awais.instagrabber.databinding.ItemSearchResultBinding;
import awais.instagrabber.db.entities.Favorite;
import awais.instagrabber.models.enums.FavoriteType;
public class FavoritesAdapter extends RecyclerView.Adapter {
private final OnFavoriteClickListener clickListener;
private final OnFavoriteLongClickListener longClickListener;
private final AsyncListDiffer differ;
private static final DiffUtil.ItemCallback diffCallback = new DiffUtil.ItemCallback() {
@Override
public boolean areItemsTheSame(@NonNull final FavoriteModelOrHeader oldItem, @NonNull final FavoriteModelOrHeader newItem) {
boolean areSame = oldItem.isHeader() && newItem.isHeader();
if (!areSame) {
return false;
}
if (oldItem.isHeader()) {
return ObjectsCompat.equals(oldItem.header, newItem.header);
}
if (oldItem.model != null && newItem.model != null) {
return oldItem.model.getId() == newItem.model.getId();
}
return false;
}
@Override
public boolean areContentsTheSame(@NonNull final FavoriteModelOrHeader oldItem, @NonNull final FavoriteModelOrHeader newItem) {
boolean areSame = oldItem.isHeader() && newItem.isHeader();
if (!areSame) {
return false;
}
if (oldItem.isHeader()) {
return ObjectsCompat.equals(oldItem.header, newItem.header);
}
return ObjectsCompat.equals(oldItem.model, newItem.model);
}
};
public FavoritesAdapter(final OnFavoriteClickListener clickListener, final OnFavoriteLongClickListener longClickListener) {
this.clickListener = clickListener;
this.longClickListener = longClickListener;
differ = new AsyncListDiffer<>(new AdapterListUpdateCallback(this),
new AsyncDifferConfig.Builder<>(diffCallback).build());
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
if (viewType == 0) {
// header
return new FavSectionViewHolder(ItemFavSectionHeaderBinding.inflate(inflater, parent, false));
}
final ItemSearchResultBinding binding = ItemSearchResultBinding.inflate(inflater, parent, false);
return new FavoriteViewHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position) {
if (getItemViewType(position) == 0) {
final FavoriteModelOrHeader modelOrHeader = getItem(position);
if (!modelOrHeader.isHeader()) return;
((FavSectionViewHolder) holder).bind(modelOrHeader.header);
return;
}
((FavoriteViewHolder) holder).bind(getItem(position).model, clickListener, longClickListener);
}
protected FavoriteModelOrHeader getItem(int position) {
return differ.getCurrentList().get(position);
}
@Override
public int getItemCount() {
return differ.getCurrentList().size();
}
@Override
public int getItemViewType(final int position) {
return getItem(position).isHeader() ? 0 : 1;
}
public void submitList(@Nullable final List list) {
if (list == null) {
differ.submitList(null);
return;
}
differ.submitList(sectionAndSort(list));
}
public void submitList(@Nullable final List list, @Nullable final Runnable commitCallback) {
if (list == null) {
differ.submitList(null, commitCallback);
return;
}
differ.submitList(sectionAndSort(list), commitCallback);
}
@NonNull
private List sectionAndSort(@NonNull final List list) {
final List listCopy = new ArrayList<>(list);
Collections.sort(listCopy, (o1, o2) -> {
if (o1.getType() == o2.getType()) return 0;
// keep users at top
if (o1.getType() == FavoriteType.USER) return -1;
if (o2.getType() == FavoriteType.USER) return 1;
// keep locations at bottom
if (o1.getType() == FavoriteType.LOCATION) return 1;
if (o2.getType() == FavoriteType.LOCATION) return -1;
return 0;
});
final List modelOrHeaders = new ArrayList<>();
for (int i = 0; i < listCopy.size(); i++) {
final Favorite model = listCopy.get(i);
final FavoriteModelOrHeader prev = modelOrHeaders.isEmpty() ? null : modelOrHeaders.get(modelOrHeaders.size() - 1);
boolean prevWasSameType = prev != null && prev.model.getType() == model.getType();
if (prevWasSameType) {
// just add model
final FavoriteModelOrHeader modelOrHeader = new FavoriteModelOrHeader();
modelOrHeader.model = model;
modelOrHeaders.add(modelOrHeader);
continue;
}
// add header and model
FavoriteModelOrHeader modelOrHeader = new FavoriteModelOrHeader();
modelOrHeader.header = model.getType();
modelOrHeaders.add(modelOrHeader);
modelOrHeader = new FavoriteModelOrHeader();
modelOrHeader.model = model;
modelOrHeaders.add(modelOrHeader);
}
return modelOrHeaders;
}
private static class FavoriteModelOrHeader {
FavoriteType header;
Favorite model;
boolean isHeader() {
return header != null;
}
}
public interface OnFavoriteClickListener {
void onClick(final Favorite model);
}
public interface OnFavoriteLongClickListener {
boolean onLongClick(final Favorite model);
}
public static class FavSectionViewHolder extends RecyclerView.ViewHolder {
private final ItemFavSectionHeaderBinding binding;
public FavSectionViewHolder(@NonNull final ItemFavSectionHeaderBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
public void bind(final FavoriteType header) {
if (header == null) return;
final int headerText;
switch (header) {
case USER:
headerText = R.string.accounts;
break;
case HASHTAG:
headerText = R.string.hashtags;
break;
case LOCATION:
headerText = R.string.locations;
break;
default:
headerText = R.string.unknown;
break;
}
binding.getRoot().setText(headerText);
}
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/FeedAdapterV2.java
================================================
package awais.instagrabber.adapters;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import awais.instagrabber.adapters.viewholder.FeedGridItemViewHolder;
import awais.instagrabber.adapters.viewholder.feed.FeedItemViewHolder;
import awais.instagrabber.adapters.viewholder.feed.FeedPhotoViewHolder;
import awais.instagrabber.adapters.viewholder.feed.FeedSliderViewHolder;
import awais.instagrabber.adapters.viewholder.feed.FeedVideoViewHolder;
import awais.instagrabber.databinding.ItemFeedGridBinding;
import awais.instagrabber.databinding.ItemFeedPhotoBinding;
import awais.instagrabber.databinding.ItemFeedSliderBinding;
import awais.instagrabber.databinding.ItemFeedVideoBinding;
import awais.instagrabber.models.PostsLayoutPreferences;
import awais.instagrabber.models.enums.MediaItemType;
import awais.instagrabber.repositories.responses.Caption;
import awais.instagrabber.repositories.responses.Media;
public final class FeedAdapterV2 extends ListAdapter {
private static final String TAG = "FeedAdapterV2";
private final FeedItemCallback feedItemCallback;
private final SelectionModeCallback selectionModeCallback;
private final Set selectedPositions = new HashSet<>();
private final Set selectedFeedModels = new HashSet<>();
private PostsLayoutPreferences layoutPreferences;
private boolean selectionModeActive = false;
private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() {
@Override
public boolean areItemsTheSame(@NonNull final Media oldItem, @NonNull final Media newItem) {
return Objects.equals(oldItem.getPk(), newItem.getPk());
}
@Override
public boolean areContentsTheSame(@NonNull final Media oldItem, @NonNull final Media newItem) {
final Caption oldItemCaption = oldItem.getCaption();
final Caption newItemCaption = newItem.getCaption();
return Objects.equals(oldItem.getPk(), newItem.getPk())
&& Objects.equals(getCaptionText(oldItemCaption), getCaptionText(newItemCaption));
}
private String getCaptionText(final Caption caption) {
if (caption == null) return null;
return caption.getText();
}
};
private final AdapterSelectionCallback adapterSelectionCallback = new AdapterSelectionCallback() {
@Override
public boolean onPostLongClick(final int position, final Media feedModel) {
if (!selectionModeActive) {
selectionModeActive = true;
notifyDataSetChanged();
if (selectionModeCallback != null) {
selectionModeCallback.onSelectionStart();
}
}
selectedPositions.add(position);
selectedFeedModels.add(feedModel);
notifyItemChanged(position);
if (selectionModeCallback != null) {
selectionModeCallback.onSelectionChange(selectedFeedModels);
}
return true;
}
@Override
public void onPostClick(final int position, final Media feedModel) {
if (!selectionModeActive) return;
if (selectedPositions.contains(position)) {
selectedPositions.remove(position);
selectedFeedModels.remove(feedModel);
} else {
selectedPositions.add(position);
selectedFeedModels.add(feedModel);
}
notifyItemChanged(position);
if (selectionModeCallback != null) {
selectionModeCallback.onSelectionChange(selectedFeedModels);
}
if (selectedPositions.isEmpty()) {
selectionModeActive = false;
notifyDataSetChanged();
if (selectionModeCallback != null) {
selectionModeCallback.onSelectionEnd();
}
}
}
};
public FeedAdapterV2(@NonNull final PostsLayoutPreferences layoutPreferences,
final FeedItemCallback feedItemCallback,
final SelectionModeCallback selectionModeCallback) {
super(DIFF_CALLBACK);
this.layoutPreferences = layoutPreferences;
this.feedItemCallback = feedItemCallback;
this.selectionModeCallback = selectionModeCallback;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final Context context = parent.getContext();
final LayoutInflater layoutInflater = LayoutInflater.from(context);
switch (layoutPreferences.getType()) {
case LINEAR:
return getLinearViewHolder(parent, layoutInflater, viewType);
case GRID:
case STAGGERED_GRID:
default:
final ItemFeedGridBinding binding = ItemFeedGridBinding.inflate(layoutInflater, parent, false);
return new FeedGridItemViewHolder(binding);
}
}
@NonNull
private RecyclerView.ViewHolder getLinearViewHolder(@NonNull final ViewGroup parent,
final LayoutInflater layoutInflater,
final int viewType) {
switch (MediaItemType.valueOf(viewType)) {
case MEDIA_TYPE_VIDEO: {
final ItemFeedVideoBinding binding = ItemFeedVideoBinding.inflate(layoutInflater, parent, false);
return new FeedVideoViewHolder(binding, feedItemCallback);
}
case MEDIA_TYPE_SLIDER: {
final ItemFeedSliderBinding binding = ItemFeedSliderBinding.inflate(layoutInflater, parent, false);
return new FeedSliderViewHolder(binding, feedItemCallback);
}
case MEDIA_TYPE_IMAGE:
default: {
final ItemFeedPhotoBinding binding = ItemFeedPhotoBinding.inflate(layoutInflater, parent, false);
return new FeedPhotoViewHolder(binding, feedItemCallback);
}
}
}
@Override
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder viewHolder, final int position) {
final Media feedModel = getItem(position);
if (feedModel == null) return;
switch (layoutPreferences.getType()) {
case LINEAR:
((FeedItemViewHolder) viewHolder).bind(feedModel);
break;
case GRID:
case STAGGERED_GRID:
default:
((FeedGridItemViewHolder) viewHolder).bind(position,
feedModel,
layoutPreferences,
feedItemCallback,
adapterSelectionCallback,
selectionModeActive,
selectedPositions.contains(position));
}
}
@Override
public int getItemViewType(final int position) {
return getItem(position).getType().getId();
}
public void setLayoutPreferences(@NonNull final PostsLayoutPreferences layoutPreferences) {
this.layoutPreferences = layoutPreferences;
}
public void endSelection() {
if (!selectionModeActive) return;
selectionModeActive = false;
selectedPositions.clear();
selectedFeedModels.clear();
notifyDataSetChanged();
if (selectionModeCallback != null) {
selectionModeCallback.onSelectionEnd();
}
}
// @Override
// public void onViewAttachedToWindow(@NonNull final FeedItemViewHolder holder) {
// super.onViewAttachedToWindow(holder);
// // Log.d(TAG, "attached holder: " + holder);
// if (!(holder instanceof FeedSliderViewHolder)) return;
// final FeedSliderViewHolder feedSliderViewHolder = (FeedSliderViewHolder) holder;
// feedSliderViewHolder.startPlayingVideo();
// }
//
// @Override
// public void onViewDetachedFromWindow(@NonNull final FeedItemViewHolder holder) {
// super.onViewDetachedFromWindow(holder);
// // Log.d(TAG, "detached holder: " + holder);
// if (!(holder instanceof FeedSliderViewHolder)) return;
// final FeedSliderViewHolder feedSliderViewHolder = (FeedSliderViewHolder) holder;
// feedSliderViewHolder.stopPlayingVideo();
// }
public interface FeedItemCallback {
void onPostClick(final Media feedModel);
void onProfilePicClick(final Media feedModel);
void onNameClick(final Media feedModel);
void onLocationClick(final Media feedModel);
void onMentionClick(final String mention);
void onHashtagClick(final String hashtag);
void onCommentsClick(final Media feedModel);
void onDownloadClick(final Media feedModel, final int childPosition, final View popupLocation);
void onEmailClick(final String emailId);
void onURLClick(final String url);
void onSliderClick(Media feedModel, int position);
}
public interface AdapterSelectionCallback {
boolean onPostLongClick(final int position, Media feedModel);
void onPostClick(final int position, Media feedModel);
}
public interface SelectionModeCallback {
void onSelectionStart();
void onSelectionChange(final Set selectedFeedModels);
void onSelectionEnd();
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/FeedItemCallbackAdapter.java
================================================
package awais.instagrabber.adapters;
import android.view.View;
import awais.instagrabber.repositories.responses.Media;
public class FeedItemCallbackAdapter implements FeedAdapterV2.FeedItemCallback {
@Override
public void onPostClick(final Media media) {}
@Override
public void onProfilePicClick(final Media media) {}
@Override
public void onNameClick(final Media media) {}
@Override
public void onLocationClick(final Media media) {}
@Override
public void onMentionClick(final String mention) {}
@Override
public void onHashtagClick(final String hashtag) {}
@Override
public void onCommentsClick(final Media media) {}
@Override
public void onDownloadClick(final Media media, final int childPosition, final View popupLocation) {}
@Override
public void onEmailClick(final String emailId) {}
@Override
public void onURLClick(final String url) {}
@Override
public void onSliderClick(final Media media, final int position) {}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/FeedStoriesAdapter.java
================================================
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import awais.instagrabber.adapters.viewholder.FeedStoryViewHolder;
import awais.instagrabber.databinding.ItemHighlightBinding;
import awais.instagrabber.repositories.responses.stories.Story;
public final class FeedStoriesAdapter extends ListAdapter {
private final OnFeedStoryClickListener listener;
private static final DiffUtil.ItemCallback diffCallback = new DiffUtil.ItemCallback() {
@Override
public boolean areItemsTheSame(@NonNull final Story oldItem, @NonNull final Story newItem) {
return oldItem.getId().equals(newItem.getId());
}
@Override
public boolean areContentsTheSame(@NonNull final Story oldItem, @NonNull final Story newItem) {
return oldItem.getId().equals(newItem.getId()) && oldItem.getSeen() == newItem.getSeen();
}
};
public FeedStoriesAdapter(final OnFeedStoryClickListener listener) {
super(diffCallback);
this.listener = listener;
}
@NonNull
@Override
public FeedStoryViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final ItemHighlightBinding binding = ItemHighlightBinding.inflate(layoutInflater, parent, false);
return new FeedStoryViewHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull final FeedStoryViewHolder holder, final int position) {
final Story model = getItem(position);
holder.bind(model, position, listener);
}
public interface OnFeedStoryClickListener {
void onFeedStoryClick(Story model, int position);
void onFeedStoryLongClick(Story model, int position);
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/FeedStoriesListAdapter.java
================================================
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.widget.Filter;
import android.widget.Filterable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import java.util.List;
import java.util.stream.Collectors;
import awais.instagrabber.adapters.viewholder.StoryListViewHolder;
import awais.instagrabber.databinding.ItemNotificationBinding;
import awais.instagrabber.repositories.responses.stories.Story;
import awais.instagrabber.utils.TextUtils;
public final class FeedStoriesListAdapter extends ListAdapter implements Filterable {
private final OnFeedStoryClickListener listener;
private List list;
private final Filter filter = new Filter() {
@NonNull
@Override
protected FilterResults performFiltering(final CharSequence filter) {
final String query = TextUtils.isEmpty(filter) ? null : filter.toString().toLowerCase();
List filteredList = list;
if (list != null && query != null) {
filteredList = list.stream()
.filter(feedStoryModel -> feedStoryModel.getUser()
.getUsername()
.toLowerCase()
.contains(query))
.collect(Collectors.toList());
}
final FilterResults filterResults = new FilterResults();
filterResults.count = filteredList != null ? filteredList.size() : 0;
filterResults.values = filteredList;
return filterResults;
}
@Override
protected void publishResults(final CharSequence constraint, final FilterResults results) {
//noinspection unchecked
submitList((List) results.values, true);
}
};
private static final DiffUtil.ItemCallback diffCallback = new DiffUtil.ItemCallback() {
@Override
public boolean areItemsTheSame(@NonNull final Story oldItem, @NonNull final Story newItem) {
return oldItem.getId().equals(newItem.getId());
}
@Override
public boolean areContentsTheSame(@NonNull final Story oldItem, @NonNull final Story newItem) {
return oldItem.getId().equals(newItem.getId()) && oldItem.getSeen() == newItem.getSeen();
}
};
public FeedStoriesListAdapter(final OnFeedStoryClickListener listener) {
super(diffCallback);
this.listener = listener;
}
@Override
public Filter getFilter() {
return filter;
}
private void submitList(@Nullable final List list, final boolean isFiltered) {
if (!isFiltered) {
this.list = list;
}
super.submitList(list);
}
@Override
public void submitList(final List list) {
submitList(list, false);
}
@NonNull
@Override
public StoryListViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final ItemNotificationBinding binding = ItemNotificationBinding.inflate(layoutInflater, parent, false);
return new StoryListViewHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull final StoryListViewHolder holder, final int position) {
final Story model = getItem(position);
holder.bind(model, listener);
}
public interface OnFeedStoryClickListener {
void onFeedStoryClick(final Story model);
void onProfileClick(final String username);
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/FiltersAdapter.java
================================================
package awais.instagrabber.adapters;
import android.graphics.Bitmap;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import java.util.Collection;
import java.util.List;
import awais.instagrabber.adapters.viewholder.FilterViewHolder;
import awais.instagrabber.databinding.ItemFilterBinding;
import awais.instagrabber.fragments.imageedit.filters.filters.Filter;
import jp.co.cyberagent.android.gpuimage.filter.GPUImageFilter;
public class FiltersAdapter extends ListAdapter, FilterViewHolder> {
private static final DiffUtil.ItemCallback> DIFF_CALLBACK = new DiffUtil.ItemCallback>() {
@Override
public boolean areItemsTheSame(@NonNull final Filter> oldItem, @NonNull final Filter> newItem) {
return oldItem.getType().equals(newItem.getType());
}
@Override
public boolean areContentsTheSame(@NonNull final Filter> oldItem, @NonNull final Filter> newItem) {
return oldItem.getType().equals(newItem.getType());
}
};
private final Bitmap bitmap;
private final OnFilterClickListener onFilterClickListener;
private final Collection filters;
private final String originalKey;
private int selectedPosition = 0;
public FiltersAdapter(final Collection filters,
final String originalKey,
final Bitmap bitmap,
final OnFilterClickListener onFilterClickListener) {
super(DIFF_CALLBACK);
this.filters = filters;
this.originalKey = originalKey;
this.bitmap = bitmap;
this.onFilterClickListener = onFilterClickListener;
setHasStableIds(true);
}
@NonNull
@Override
public FilterViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final ItemFilterBinding binding = ItemFilterBinding.inflate(layoutInflater, parent, false);
return new FilterViewHolder(binding, filters, onFilterClickListener);
}
@Override
public void onBindViewHolder(@NonNull final FilterViewHolder holder, final int position) {
holder.bind(position, originalKey, bitmap, getItem(position), selectedPosition == position);
}
@Override
public long getItemId(final int position) {
return getItem(position).getLabel();
}
public void setSelected(final int position) {
final int prev = this.selectedPosition;
this.selectedPosition = position;
notifyItemChanged(position);
notifyItemChanged(prev);
}
public void setSelectedFilter(final GPUImageFilter instance) {
final List> currentList = getCurrentList();
int index = -1;
for (int i = 0; i < currentList.size(); i++) {
final Filter> filter = currentList.get(i);
final GPUImageFilter filterInstance = filter.getInstance();
if (filterInstance.getClass() == instance.getClass()) {
index = i;
break;
}
}
if (index < 0) return;
setSelected(index);
}
public interface OnFilterClickListener {
void onClick(int position, Filter> filter);
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/FollowAdapter.java
================================================
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Filter;
import android.widget.Filterable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import awais.instagrabber.R;
import awais.instagrabber.adapters.viewholder.FollowsViewHolder;
import awais.instagrabber.databinding.ItemFollowBinding;
import awais.instagrabber.interfaces.OnGroupClickListener;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.utils.TextUtils;
import thoughtbot.expandableadapter.ExpandableGroup;
import thoughtbot.expandableadapter.ExpandableList;
import thoughtbot.expandableadapter.ExpandableListPosition;
import thoughtbot.expandableadapter.GroupViewHolder;
// thanks to ThoughtBot's ExpandableRecyclerViewAdapter
// https://github.com/thoughtbot/expandable-recycler-view
public final class FollowAdapter extends RecyclerView.Adapter implements OnGroupClickListener, Filterable {
private final View.OnClickListener onClickListener;
private final ExpandableList expandableListOriginal;
private final boolean hasManyGroups;
private ExpandableList expandableList;
private final Filter filter = new Filter() {
@Nullable
@Override
protected FilterResults performFiltering(final CharSequence filter) {
final List filteredItems = new ArrayList();
if (expandableListOriginal.groups == null || TextUtils.isEmpty(filter)) return null;
final String query = filter.toString().toLowerCase();
final ArrayList groups = new ArrayList();
for (int x = 0; x < expandableListOriginal.groups.size(); ++x) {
final ExpandableGroup expandableGroup = expandableListOriginal.groups.get(x);
final String title = expandableGroup.getTitle();
final List items = expandableGroup.getItems();
if (items != null) {
final List toReturn = items.stream()
.filter(u -> hasKey(query, u.getUsername(), u.getFullName()))
.collect(Collectors.toList());
groups.add(new ExpandableGroup(title, toReturn));
}
}
final FilterResults filterResults = new FilterResults();
filterResults.values = new ExpandableList(groups, expandableList.expandedGroupIndexes);
return filterResults;
}
private boolean hasKey(final String key, final String username, final String name) {
if (TextUtils.isEmpty(key)) return true;
final boolean hasUserName = username != null && username.toLowerCase().contains(key);
if (!hasUserName && name != null) return name.toLowerCase().contains(key);
return true;
}
@Override
protected void publishResults(final CharSequence constraint, final FilterResults results) {
if (results == null) {
expandableList = expandableListOriginal;
}
else {
final ExpandableList filteredList = (ExpandableList) results.values;
expandableList = filteredList;
}
notifyDataSetChanged();
}
};
public FollowAdapter(final View.OnClickListener onClickListener, @NonNull final ArrayList groups) {
this.expandableListOriginal = new ExpandableList(groups);
expandableList = this.expandableListOriginal;
this.onClickListener = onClickListener;
this.hasManyGroups = groups.size() > 1;
}
@Override
public Filter getFilter() {
return filter;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final boolean isGroup = hasManyGroups && viewType == ExpandableListPosition.GROUP;
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final View view;
if (isGroup) {
view = layoutInflater.inflate(R.layout.header_follow, parent, false);
return new GroupViewHolder(view, this);
} else {
final ItemFollowBinding binding = ItemFollowBinding.inflate(layoutInflater, parent, false);
return new FollowsViewHolder(binding);
}
}
@Override
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position) {
final ExpandableListPosition listPos = expandableList.getUnflattenedPosition(position);
final ExpandableGroup group = expandableList.getExpandableGroup(listPos);
if (hasManyGroups && listPos.type == ExpandableListPosition.GROUP) {
final GroupViewHolder gvh = (GroupViewHolder) holder;
gvh.setTitle(group.getTitle());
gvh.toggle(isGroupExpanded(group));
return;
}
final User model = group.getItems().get(hasManyGroups ? listPos.childPos : position);
((FollowsViewHolder) holder).bind(model, onClickListener);
}
@Override
public int getItemCount() {
return expandableList.getVisibleItemCount() - (hasManyGroups ? 0 : 1);
}
@Override
public int getItemViewType(final int position) {
return !hasManyGroups ? 0 : expandableList.getUnflattenedPosition(position).type;
}
@Override
public void toggleGroup(final int flatPos) {
final ExpandableListPosition listPosition = expandableList.getUnflattenedPosition(flatPos);
final int groupPos = listPosition.groupPos;
final int positionStart = expandableList.getFlattenedGroupIndex(listPosition) + 1;
final int positionEnd = expandableList.groups.get(groupPos).getItemCount();
final boolean isExpanded = expandableList.expandedGroupIndexes[groupPos];
expandableList.expandedGroupIndexes[groupPos] = !isExpanded;
notifyItemChanged(positionStart - 1);
if (positionEnd > 0) {
if (isExpanded) notifyItemRangeRemoved(positionStart, positionEnd);
else notifyItemRangeInserted(positionStart, positionEnd);
}
}
public boolean isGroupExpanded(final ExpandableGroup group) {
return expandableList.expandedGroupIndexes[expandableList.groups.indexOf(group)];
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/GifItemsAdapter.java
================================================
package awais.instagrabber.adapters;
import android.net.Uri;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.drawee.backends.pipeline.PipelineDraweeControllerBuilder;
import com.facebook.drawee.controller.BaseControllerListener;
import com.facebook.drawee.drawable.ScalingUtils;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
import com.facebook.imagepipeline.common.ResizeOptions;
import com.facebook.imagepipeline.image.ImageInfo;
import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.imagepipeline.request.ImageRequestBuilder;
import java.util.Objects;
import awais.instagrabber.databinding.ItemMediaBinding;
import awais.instagrabber.repositories.responses.giphy.GiphyGif;
import awais.instagrabber.utils.Utils;
public class GifItemsAdapter extends ListAdapter {
private static final DiffUtil.ItemCallback diffCallback = new DiffUtil.ItemCallback() {
@Override
public boolean areItemsTheSame(@NonNull final GiphyGif oldItem, @NonNull final GiphyGif newItem) {
return Objects.equals(oldItem.getId(), newItem.getId());
}
@Override
public boolean areContentsTheSame(@NonNull final GiphyGif oldItem, @NonNull final GiphyGif newItem) {
return Objects.equals(oldItem.getId(), newItem.getId());
}
};
private final OnItemClickListener onItemClickListener;
public GifItemsAdapter(final OnItemClickListener onItemClickListener) {
super(diffCallback);
this.onItemClickListener = onItemClickListener;
}
@NonNull
@Override
public GifViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final ItemMediaBinding binding = ItemMediaBinding.inflate(layoutInflater, parent, false);
return new GifViewHolder(binding, onItemClickListener);
}
@Override
public void onBindViewHolder(@NonNull final GifViewHolder holder, final int position) {
holder.bind(getItem(position));
}
public static class GifViewHolder extends RecyclerView.ViewHolder {
private static final String TAG = GifViewHolder.class.getSimpleName();
private static final int size = Utils.displayMetrics.widthPixels / 3;
private final ItemMediaBinding binding;
private final OnItemClickListener onItemClickListener;
public GifViewHolder(@NonNull final ItemMediaBinding binding,
final OnItemClickListener onItemClickListener) {
super(binding.getRoot());
this.binding = binding;
this.onItemClickListener = onItemClickListener;
binding.duration.setVisibility(View.GONE);
final GenericDraweeHierarchyBuilder builder = new GenericDraweeHierarchyBuilder(itemView.getResources());
builder.setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER);
binding.item.setHierarchy(builder.build());
}
public void bind(final GiphyGif item) {
if (onItemClickListener != null) {
itemView.setOnClickListener(v -> onItemClickListener.onItemClick(item));
}
final BaseControllerListener controllerListener = new BaseControllerListener() {
@Override
public void onFailure(final String id, final Throwable throwable) {
Log.e(TAG, "onFailure: ", throwable);
}
};
final ImageRequest request = ImageRequestBuilder
.newBuilderWithSource(Uri.parse(item.getImages().getFixedHeight().getWebp()))
.setResizeOptions(ResizeOptions.forDimensions(size, size))
.build();
final PipelineDraweeControllerBuilder builder = Fresco.newDraweeControllerBuilder()
.setImageRequest(request)
.setAutoPlayAnimations(true)
.setControllerListener(controllerListener);
binding.item.setController(builder.build());
}
}
public interface OnItemClickListener {
void onItemClick(GiphyGif giphyGif);
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/HighlightStoriesListAdapter.java
================================================
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import awais.instagrabber.adapters.viewholder.StoryListViewHolder;
import awais.instagrabber.databinding.ItemNotificationBinding;
import awais.instagrabber.repositories.responses.stories.Story;
public final class HighlightStoriesListAdapter extends ListAdapter {
private final OnHighlightStoryClickListener listener;
private static final DiffUtil.ItemCallback diffCallback = new DiffUtil.ItemCallback() {
@Override
public boolean areItemsTheSame(@NonNull final Story oldItem, @NonNull final Story newItem) {
return oldItem.getId().equals(newItem.getId());
}
@Override
public boolean areContentsTheSame(@NonNull final Story oldItem, @NonNull final Story newItem) {
return oldItem.getId().equals(newItem.getId());
}
};
public HighlightStoriesListAdapter(final OnHighlightStoryClickListener listener) {
super(diffCallback);
this.listener = listener;
}
@NonNull
@Override
public StoryListViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final ItemNotificationBinding binding = ItemNotificationBinding.inflate(layoutInflater, parent, false);
return new StoryListViewHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull final StoryListViewHolder holder, final int position) {
final Story model = getItem(position);
holder.bind(model, position, listener);
}
public interface OnHighlightStoryClickListener {
void onHighlightClick(final Story model, final int position);
void onProfileClick(final String username);
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/HighlightsAdapter.java
================================================
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import awais.instagrabber.adapters.viewholder.HighlightViewHolder;
import awais.instagrabber.databinding.ItemHighlightBinding;
import awais.instagrabber.repositories.responses.stories.Story;
public final class HighlightsAdapter extends ListAdapter {
private final OnHighlightClickListener clickListener;
private static final DiffUtil.ItemCallback diffCallback = new DiffUtil.ItemCallback() {
@Override
public boolean areItemsTheSame(@NonNull final Story oldItem, @NonNull final Story newItem) {
return oldItem.getId().equals(newItem.getId());
}
@Override
public boolean areContentsTheSame(@NonNull final Story oldItem, @NonNull final Story newItem) {
return oldItem.getId().equals(newItem.getId());
}
};
public HighlightsAdapter(final OnHighlightClickListener clickListener) {
super(diffCallback);
this.clickListener = clickListener;
}
@NonNull
@Override
public HighlightViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final ItemHighlightBinding binding = ItemHighlightBinding.inflate(layoutInflater, parent, false);
return new HighlightViewHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull final HighlightViewHolder holder, final int position) {
final Story highlightModel = getItem(position);
if (clickListener != null) {
holder.itemView.setOnClickListener(v -> clickListener.onHighlightClick(highlightModel, position));
}
holder.bind(highlightModel);
}
public interface OnHighlightClickListener {
void onHighlightClick(final Story model, final int position);
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/KeywordsFilterAdapter.java
================================================
package awais.instagrabber.adapters;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import awais.instagrabber.R;
import awais.instagrabber.adapters.viewholder.dialogs.KeywordsFilterDialogViewHolder;
public class KeywordsFilterAdapter extends RecyclerView.Adapter {
private final Context context;
private final ArrayList items;
public KeywordsFilterAdapter(Context context, ArrayList items){
this.context = context;
this.items = items;
}
@NonNull
@Override
public KeywordsFilterDialogViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
final View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_keyword, parent, false);
return new KeywordsFilterDialogViewHolder(v);
}
@Override
public void onBindViewHolder(@NonNull KeywordsFilterDialogViewHolder holder, int position) {
holder.bind(items, position, context, this);
}
@Override
public int getItemCount() {
return items.size();
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/LikesAdapter.java
================================================
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
import awais.instagrabber.adapters.viewholder.FollowsViewHolder;
import awais.instagrabber.databinding.ItemFollowBinding;
import awais.instagrabber.repositories.responses.User;
public final class LikesAdapter extends RecyclerView.Adapter {
private final List profileModels;
private final View.OnClickListener onClickListener;
public LikesAdapter(final List profileModels,
final View.OnClickListener onClickListener) {
this.profileModels = profileModels;
this.onClickListener = onClickListener;
}
@NonNull
@Override
public FollowsViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final ItemFollowBinding binding = ItemFollowBinding.inflate(layoutInflater, parent, false);
return new FollowsViewHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull final FollowsViewHolder holder, final int position) {
final User model = profileModels.get(position);
holder.bind(model, onClickListener);
}
@Override
public int getItemCount() {
return profileModels.size();
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/NotificationsAdapter.java
================================================
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import awais.instagrabber.adapters.viewholder.NotificationViewHolder;
import awais.instagrabber.databinding.ItemNotificationBinding;
import awais.instagrabber.models.enums.NotificationType;
import awais.instagrabber.repositories.responses.notification.Notification;
public final class NotificationsAdapter extends ListAdapter {
private final OnNotificationClickListener notificationClickListener;
private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() {
@Override
public boolean areItemsTheSame(final Notification oldItem, final Notification newItem) {
return Objects.requireNonNull(oldItem.getPk()).equals(newItem.getPk());
}
@Override
public boolean areContentsTheSame(@NonNull final Notification oldItem, @NonNull final Notification newItem) {
return Objects.requireNonNull(oldItem.getPk()).equals(newItem.getPk()) && Objects.equals(oldItem.getType(), newItem.getType());
}
};
public NotificationsAdapter(final OnNotificationClickListener notificationClickListener) {
super(DIFF_CALLBACK);
this.notificationClickListener = notificationClickListener;
}
@NonNull
@Override
public NotificationViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int type) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final ItemNotificationBinding binding = ItemNotificationBinding.inflate(layoutInflater, parent, false);
return new NotificationViewHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull final NotificationViewHolder holder, final int position) {
final Notification Notification = getItem(position);
holder.bind(Notification, notificationClickListener);
}
@Override
public void submitList(@Nullable final List list, @Nullable final Runnable commitCallback) {
if (list == null) {
super.submitList(null, commitCallback);
return;
}
super.submitList(sort(list), commitCallback);
}
@Override
public void submitList(@Nullable final List list) {
if (list == null) {
super.submitList(null);
return;
}
super.submitList(sort(list));
}
private List sort(final List list) {
final List listCopy = new ArrayList<>(list).stream()
.filter(i -> i.getType() != null)
.collect(Collectors.toList());
Collections.sort(listCopy, (o1, o2) -> {
// keep requests at top
if (o1.getType() == o2.getType()
&& o1.getType() == NotificationType.REQUEST
&& o2.getType() == NotificationType.REQUEST) return 0;
else if (o1.getType() == NotificationType.REQUEST) return -1;
else if (o2.getType() == NotificationType.REQUEST) return 1;
// timestamp
return Double.compare(o2.getArgs().getTimestamp(), o1.getArgs().getTimestamp());
});
return listCopy;
}
public interface OnNotificationClickListener {
void onNotificationClick(final Notification model);
void onProfileClick(final String username);
void onPreviewClick(final Notification model);
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/SavedCollectionsAdapter.java
================================================
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import java.util.Objects;
import awais.instagrabber.adapters.viewholder.TopicClusterViewHolder;
import awais.instagrabber.databinding.ItemDiscoverTopicBinding;
import awais.instagrabber.repositories.responses.saved.SavedCollection;
public class SavedCollectionsAdapter extends ListAdapter {
private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() {
@Override
public boolean areItemsTheSame(@NonNull final SavedCollection oldItem, @NonNull final SavedCollection newItem) {
return oldItem.getCollectionId().equals(newItem.getCollectionId());
}
@Override
public boolean areContentsTheSame(@NonNull final SavedCollection oldItem, @NonNull final SavedCollection newItem) {
if (oldItem.getCoverMediaList() != null && newItem.getCoverMediaList() != null
&& oldItem.getCoverMediaList().size() == newItem.getCoverMediaList().size()) {
return Objects.equals(oldItem.getCoverMediaList().get(0).getId(), newItem.getCoverMediaList().get(0).getId());
}
else if (oldItem.getCoverMedia() != null && newItem.getCoverMedia() != null) {
return Objects.equals(oldItem.getCoverMedia().getId(), newItem.getCoverMedia().getId());
}
return false;
}
};
private final OnCollectionClickListener onCollectionClickListener;
public SavedCollectionsAdapter(final OnCollectionClickListener onCollectionClickListener) {
super(DIFF_CALLBACK);
this.onCollectionClickListener = onCollectionClickListener;
}
@NonNull
@Override
public TopicClusterViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final ItemDiscoverTopicBinding binding = ItemDiscoverTopicBinding.inflate(layoutInflater, parent, false);
return new TopicClusterViewHolder(binding, null, onCollectionClickListener);
}
@Override
public void onBindViewHolder(@NonNull final TopicClusterViewHolder holder, final int position) {
final SavedCollection topicCluster = getItem(position);
holder.bind(topicCluster);
}
public interface OnCollectionClickListener {
void onCollectionClick(SavedCollection savedCollection, View root, View cover, View title, int titleColor, int backgroundColor);
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/SearchCategoryAdapter.java
================================================
package awais.instagrabber.adapters;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import java.util.List;
import awais.instagrabber.fragments.search.SearchCategoryFragment;
import awais.instagrabber.models.enums.FavoriteType;
public class SearchCategoryAdapter extends FragmentStateAdapter {
private final List categories;
public SearchCategoryAdapter(@NonNull final Fragment fragment,
@NonNull final List categories) {
super(fragment);
this.categories = categories;
}
@NonNull
@Override
public Fragment createFragment(final int position) {
return SearchCategoryFragment.newInstance(categories.get(position));
}
@Override
public int getItemCount() {
return categories.size();
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/SearchItemsAdapter.java
================================================
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.AdapterListUpdateCallback;
import androidx.recyclerview.widget.AsyncDifferConfig;
import androidx.recyclerview.widget.AsyncListDiffer;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import awais.instagrabber.R;
import awais.instagrabber.adapters.viewholder.SearchItemViewHolder;
import awais.instagrabber.databinding.ItemFavSectionHeaderBinding;
import awais.instagrabber.databinding.ItemSearchResultBinding;
import awais.instagrabber.fragments.search.SearchCategoryFragment.OnSearchItemClickListener;
import awais.instagrabber.models.enums.FavoriteType;
import awais.instagrabber.repositories.responses.search.SearchItem;
public final class SearchItemsAdapter extends RecyclerView.Adapter {
private static final String TAG = SearchItemsAdapter.class.getSimpleName();
private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() {
@Override
public boolean areItemsTheSame(@NonNull final SearchItemOrHeader oldItem, @NonNull final SearchItemOrHeader newItem) {
return Objects.equals(oldItem, newItem);
}
@Override
public boolean areContentsTheSame(@NonNull final SearchItemOrHeader oldItem, @NonNull final SearchItemOrHeader newItem) {
return Objects.equals(oldItem, newItem);
}
};
private static final String RECENT = "recent";
private static final String FAVORITE = "favorite";
private static final int VIEW_TYPE_HEADER = 0;
private static final int VIEW_TYPE_ITEM = 1;
private final OnSearchItemClickListener onSearchItemClickListener;
private final AsyncListDiffer differ;
public SearchItemsAdapter(final OnSearchItemClickListener onSearchItemClickListener) {
differ = new AsyncListDiffer<>(new AdapterListUpdateCallback(this),
new AsyncDifferConfig.Builder<>(DIFF_CALLBACK).build());
this.onSearchItemClickListener = onSearchItemClickListener;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
if (viewType == VIEW_TYPE_HEADER) {
return new HeaderViewHolder(ItemFavSectionHeaderBinding.inflate(layoutInflater, parent, false));
}
final ItemSearchResultBinding binding = ItemSearchResultBinding.inflate(layoutInflater, parent, false);
return new SearchItemViewHolder(binding, onSearchItemClickListener);
}
@Override
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position) {
if (getItemViewType(position) == VIEW_TYPE_HEADER) {
final SearchItemOrHeader searchItemOrHeader = getItem(position);
if (!searchItemOrHeader.isHeader()) return;
((HeaderViewHolder) holder).bind(searchItemOrHeader.header);
return;
}
((SearchItemViewHolder) holder).bind(getItem(position).searchItem);
}
protected SearchItemOrHeader getItem(int position) {
return differ.getCurrentList().get(position);
}
@Override
public int getItemCount() {
return differ.getCurrentList().size();
}
@Override
public int getItemViewType(final int position) {
return getItem(position).isHeader() ? VIEW_TYPE_HEADER : VIEW_TYPE_ITEM;
}
public void submitList(@Nullable final List list) {
if (list == null) {
differ.submitList(null);
return;
}
differ.submitList(sectionAndSort(list));
}
public void submitList(@Nullable final List list, @Nullable final Runnable commitCallback) {
if (list == null) {
differ.submitList(null, commitCallback);
return;
}
differ.submitList(sectionAndSort(list), commitCallback);
}
@NonNull
private List sectionAndSort(@NonNull final List list) {
final boolean containsRecentOrFavorite = list.stream().anyMatch(searchItem -> searchItem.isRecent() || searchItem.isFavorite());
// Don't do anything if not showing recent results
if (!containsRecentOrFavorite) {
return list.stream()
.map(SearchItemOrHeader::new)
.collect(Collectors.toList());
}
final List listCopy = new ArrayList<>(list);
Collections.sort(listCopy, (o1, o2) -> {
final boolean bothRecent = o1.isRecent() && o2.isRecent();
if (bothRecent) {
// Don't sort
return 0;
}
final boolean bothFavorite = o1.isFavorite() && o2.isFavorite();
if (bothFavorite) {
if (o1.getType() == o2.getType()) return 0;
// keep users at top
if (o1.getType() == FavoriteType.USER) return -1;
if (o2.getType() == FavoriteType.USER) return 1;
// keep locations at bottom
if (o1.getType() == FavoriteType.LOCATION) return 1;
if (o2.getType() == FavoriteType.LOCATION) return -1;
}
// keep recents at top
if (o1.isRecent()) return -1;
if (o2.isRecent()) return 1;
return 0;
});
final List itemOrHeaders = new ArrayList<>();
for (int i = 0; i < listCopy.size(); i++) {
final SearchItem searchItem = listCopy.get(i);
final SearchItemOrHeader prev = itemOrHeaders.isEmpty() ? null : itemOrHeaders.get(itemOrHeaders.size() - 1);
boolean prevWasSameType = prev != null && ((prev.searchItem.isRecent() && searchItem.isRecent())
|| (prev.searchItem.isFavorite() && searchItem.isFavorite()));
if (prevWasSameType) {
// just add the item
itemOrHeaders.add(new SearchItemOrHeader(searchItem));
continue;
}
// add header and item
// add header only if search item is recent or favorite
if (searchItem.isRecent() || searchItem.isFavorite()) {
itemOrHeaders.add(new SearchItemOrHeader(searchItem.isRecent() ? RECENT : FAVORITE));
}
itemOrHeaders.add(new SearchItemOrHeader(searchItem));
}
return itemOrHeaders;
}
private static class SearchItemOrHeader {
String header;
SearchItem searchItem;
public SearchItemOrHeader(final SearchItem searchItem) {
this.searchItem = searchItem;
}
public SearchItemOrHeader(final String header) {
this.header = header;
}
boolean isHeader() {
return header != null;
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final SearchItemOrHeader that = (SearchItemOrHeader) o;
return Objects.equals(header, that.header) &&
Objects.equals(searchItem, that.searchItem);
}
@Override
public int hashCode() {
return Objects.hash(header, searchItem);
}
}
public static class HeaderViewHolder extends RecyclerView.ViewHolder {
private final ItemFavSectionHeaderBinding binding;
public HeaderViewHolder(@NonNull final ItemFavSectionHeaderBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
public void bind(final String header) {
if (header == null) return;
final int headerText;
switch (header) {
case RECENT:
headerText = R.string.recent;
break;
case FAVORITE:
headerText = R.string.title_favorites;
break;
default:
headerText = R.string.unknown;
break;
}
binding.getRoot().setText(headerText);
}
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/SliderCallbackAdapter.java
================================================
package awais.instagrabber.adapters;
import android.view.View;
import com.google.android.exoplayer2.ui.StyledPlayerView;
import awais.instagrabber.repositories.responses.Media;
public class SliderCallbackAdapter implements SliderItemsAdapter.SliderCallback {
@Override
public void onThumbnailLoaded(final int position) {}
@Override
public void onItemClicked(final int position, final Media media, final View view) {}
@Override
public void onPlayerPlay(final int position) {}
@Override
public void onPlayerPause(final int position) {}
@Override
public void onPlayerRelease(final int position) {}
@Override
public void onFullScreenModeChanged(final boolean isFullScreen, final StyledPlayerView playerView) {}
@Override
public boolean isInFullScreen() {
return false;
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/SliderItemsAdapter.java
================================================
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import com.google.android.exoplayer2.ui.StyledPlayerView;
import awais.instagrabber.adapters.viewholder.SliderItemViewHolder;
import awais.instagrabber.adapters.viewholder.SliderPhotoViewHolder;
import awais.instagrabber.adapters.viewholder.SliderVideoViewHolder;
import awais.instagrabber.databinding.ItemSliderPhotoBinding;
import awais.instagrabber.databinding.LayoutVideoPlayerWithThumbnailBinding;
import awais.instagrabber.models.enums.MediaItemType;
import awais.instagrabber.repositories.responses.Media;
public final class SliderItemsAdapter extends ListAdapter {
private final boolean loadVideoOnItemClick;
private final SliderCallback sliderCallback;
private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() {
@Override
public boolean areItemsTheSame(@NonNull final Media oldItem, @NonNull final Media newItem) {
return oldItem.getPk().equals(newItem.getPk());
}
@Override
public boolean areContentsTheSame(@NonNull final Media oldItem, @NonNull final Media newItem) {
return oldItem.getPk().equals(newItem.getPk());
}
};
public SliderItemsAdapter(final boolean loadVideoOnItemClick,
final SliderCallback sliderCallback) {
super(DIFF_CALLBACK);
this.loadVideoOnItemClick = loadVideoOnItemClick;
this.sliderCallback = sliderCallback;
}
@NonNull
@Override
public SliderItemViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
final MediaItemType mediaItemType = MediaItemType.valueOf(viewType);
switch (mediaItemType) {
case MEDIA_TYPE_VIDEO: {
final LayoutVideoPlayerWithThumbnailBinding binding = LayoutVideoPlayerWithThumbnailBinding.inflate(inflater, parent, false);
return new SliderVideoViewHolder(binding, loadVideoOnItemClick);
}
case MEDIA_TYPE_IMAGE:
default:
final ItemSliderPhotoBinding binding = ItemSliderPhotoBinding.inflate(inflater, parent, false);
return new SliderPhotoViewHolder(binding);
}
}
@Override
public void onBindViewHolder(@NonNull final SliderItemViewHolder holder, final int position) {
final Media media = getItem(position);
holder.bind(media, position, sliderCallback);
}
@Override
public int getItemViewType(final int position) {
final Media media = getItem(position);
return media.getType().getId();
}
// @NonNull
// @Override
// public Object instantiateItem(@NonNull final ViewGroup container, final int position) {
// final Context context = container.getContext();
// final ViewerPostModel sliderItem = sliderItems.get(position);
//
// if (sliderItem.getItemType() == MediaItemType.MEDIA_TYPE_VIDEO) {
// final ViewSwitcher viewSwitcher = createViewSwitcher(context, position, sliderItem.getThumbnailUrl(), sliderItem.getDisplayUrl());
// container.addView(viewSwitcher);
// return viewSwitcher;
// }
// final GenericDraweeHierarchy hierarchy = GenericDraweeHierarchyBuilder.newInstance(container.getResources())
// .setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER)
// .build();
// final SimpleDraweeView photoView = new SimpleDraweeView(context, hierarchy);
// photoView.setLayoutParams(layoutParams);
// final ImageRequest imageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(sliderItem.getDisplayUrl()))
// .setLocalThumbnailPreviewsEnabled(true)
// .setProgressiveRenderingEnabled(true)
// .build();
// photoView.setImageRequest(imageRequest);
// container.addView(photoView);
// return photoView;
// }
// @NonNull
// private ViewSwitcher createViewSwitcher(final Context context,
// final int position,
// final String thumbnailUrl,
// final String displayUrl) {
//
// final ViewSwitcher viewSwitcher = new ViewSwitcher(context);
// viewSwitcher.setLayoutParams(layoutParams);
//
// final FrameLayout frameLayout = new FrameLayout(context);
// frameLayout.setLayoutParams(layoutParams);
//
// final GenericDraweeHierarchy hierarchy = new GenericDraweeHierarchyBuilder(context.getResources())
// .setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER)
// .build();
// final SimpleDraweeView simpleDraweeView = new SimpleDraweeView(context, hierarchy);
// simpleDraweeView.setLayoutParams(layoutParams);
// simpleDraweeView.setImageURI(thumbnailUrl);
// frameLayout.addView(simpleDraweeView);
//
// final AppCompatImageView imageView = new AppCompatImageView(context);
// final int px = Utils.convertDpToPx(50);
// final FrameLayout.LayoutParams playButtonLayoutParams = new FrameLayout.LayoutParams(px, px);
// playButtonLayoutParams.gravity = Gravity.CENTER;
// imageView.setLayoutParams(playButtonLayoutParams);
// imageView.setImageResource(R.drawable.exo_icon_play);
// frameLayout.addView(imageView);
//
// viewSwitcher.addView(frameLayout);
//
// final PlayerView playerView = new PlayerView(context);
// viewSwitcher.addView(playerView);
// if (shouldAutoPlay && position == 0) {
// loadPlayer(context, position, displayUrl, viewSwitcher, factory, playerChangeListener);
// } else
// frameLayout.setOnClickListener(v -> loadPlayer(context, position, displayUrl, viewSwitcher, factory, playerChangeListener));
// return viewSwitcher;
// }
public interface SliderCallback {
void onThumbnailLoaded(int position);
void onItemClicked(int position, final Media media, final View view);
void onPlayerPlay(int position);
void onPlayerPause(int position);
void onPlayerRelease(int position);
void onFullScreenModeChanged(boolean isFullScreen, final StyledPlayerView playerView);
boolean isInFullScreen();
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/StoriesAdapter.java
================================================
package awais.instagrabber.adapters;
import java.util.List;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.databinding.ItemStoryBinding;
import awais.instagrabber.repositories.responses.stories.StoryMedia;
import awais.instagrabber.utils.ResponseBodyUtils;
public final class StoriesAdapter extends ListAdapter {
private final OnItemClickListener onItemClickListener;
private static final DiffUtil.ItemCallback diffCallback = new DiffUtil.ItemCallback() {
@Override
public boolean areItemsTheSame(@NonNull final StoryMedia oldItem, @NonNull final StoryMedia newItem) {
return oldItem.getId().equals(newItem.getId());
}
@Override
public boolean areContentsTheSame(@NonNull final StoryMedia oldItem, @NonNull final StoryMedia newItem) {
return oldItem.getId().equals(newItem.getId());
}
};
public StoriesAdapter(final OnItemClickListener onItemClickListener) {
super(diffCallback);
this.onItemClickListener = onItemClickListener;
}
@NonNull
@Override
public StoryViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final ItemStoryBinding binding = ItemStoryBinding.inflate(layoutInflater, parent, false);
return new StoryViewHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull final StoryViewHolder holder, final int position) {
final StoryMedia storyMedia = getItem(position);
holder.bind(storyMedia, position, onItemClickListener);
}
public final static class StoryViewHolder extends RecyclerView.ViewHolder {
private final ItemStoryBinding binding;
public StoryViewHolder(final ItemStoryBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
public void bind(final StoryMedia model,
final int position,
final OnItemClickListener clickListener) {
if (model == null) return;
model.setPosition(position);
itemView.setTag(model);
itemView.setOnClickListener(v -> {
if (clickListener == null) return;
clickListener.onItemClick(model, position);
});
binding.selectedView.setVisibility(model.isCurrentSlide() ? View.VISIBLE : View.GONE);
binding.icon.setImageURI(ResponseBodyUtils.getThumbUrl(model));
}
}
public void paginate(final int newIndex) {
final List list = getCurrentList();
for (int i = 0; i < list.size(); i++) {
final StoryMedia item = list.get(i);
if (!item.isCurrentSlide() && i != newIndex) continue;
item.setCurrentSlide(i == newIndex);
notifyItemChanged(i, item);
}
}
public interface OnItemClickListener {
void onItemClick(StoryMedia storyModel, int position);
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/TabsAdapter.java
================================================
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import awais.instagrabber.R;
import awais.instagrabber.adapters.viewholder.TabViewHolder;
import awais.instagrabber.databinding.ItemFavSectionHeaderBinding;
import awais.instagrabber.databinding.ItemTabOrderPrefBinding;
import awais.instagrabber.models.Tab;
import awais.instagrabber.utils.Utils;
public class TabsAdapter extends ListAdapter {
private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() {
@Override
public boolean areItemsTheSame(@NonNull final TabOrHeader oldItem, @NonNull final TabOrHeader newItem) {
if (oldItem.isHeader() && newItem.isHeader()) {
return oldItem.header == newItem.header;
}
if (!oldItem.isHeader() && !newItem.isHeader()) {
final Tab oldTab = oldItem.tab;
final Tab newTab = newItem.tab;
return oldTab.getIconResId() == newTab.getIconResId()
&& Objects.equals(oldTab.getTitle(), newTab.getTitle());
}
return false;
}
@Override
public boolean areContentsTheSame(@NonNull final TabOrHeader oldItem, @NonNull final TabOrHeader newItem) {
if (oldItem.isHeader() && newItem.isHeader()) {
return oldItem.header == newItem.header;
}
if (!oldItem.isHeader() && !newItem.isHeader()) {
final Tab oldTab = oldItem.tab;
final Tab newTab = newItem.tab;
return oldTab.getIconResId() == newTab.getIconResId()
&& Objects.equals(oldTab.getTitle(), newTab.getTitle());
}
return false;
}
};
private final TabAdapterCallback tabAdapterCallback;
private List current = new ArrayList<>();
private List others = new ArrayList<>();
public TabsAdapter(@NonNull final TabAdapterCallback tabAdapterCallback) {
super(DIFF_CALLBACK);
this.tabAdapterCallback = tabAdapterCallback;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
if (viewType == 1) {
final ItemTabOrderPrefBinding binding = ItemTabOrderPrefBinding.inflate(layoutInflater, parent, false);
return new TabViewHolder(binding, tabAdapterCallback);
}
final ItemFavSectionHeaderBinding headerBinding = ItemFavSectionHeaderBinding.inflate(layoutInflater, parent, false);
return new DirectUsersAdapter.HeaderViewHolder(headerBinding);
}
@Override
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position) {
if (holder instanceof DirectUsersAdapter.HeaderViewHolder) {
((DirectUsersAdapter.HeaderViewHolder) holder).bind(R.string.other_tabs);
return;
}
if (holder instanceof TabViewHolder) {
final Tab tab = getItem(position).tab;
((TabViewHolder) holder).bind(tab, others.contains(tab), current.size() == 5);
}
}
@Override
public int getItemViewType(final int position) {
return getItem(position).isHeader() ? 0 : 1;
}
public void submitList(final List current, final List others, final Runnable commitCallback) {
final ImmutableList.Builder builder = ImmutableList.builder();
if (current != null) {
builder.addAll(current.stream()
.map(TabOrHeader::new)
.collect(Collectors.toList()));
}
builder.add(new TabOrHeader(R.string.other_tabs));
if (others != null) {
builder.addAll(others.stream()
.map(TabOrHeader::new)
.collect(Collectors.toList()));
}
// Mutable non-null copies
this.current = current != null ? new ArrayList<>(current) : new ArrayList<>();
this.others = others != null ? new ArrayList<>(others) : new ArrayList<>();
submitList(builder.build(), commitCallback);
}
public void submitList(final List current, final List others) {
submitList(current, others, null);
}
public void moveItem(final int from, final int to) {
final List currentCopy = new ArrayList<>(current);
Utils.moveItem(from, to, currentCopy);
submitList(currentCopy, others);
tabAdapterCallback.onOrderChange(currentCopy);
}
public int getCurrentCount() {
return current.size();
}
public static class TabOrHeader {
Tab tab;
int header;
public TabOrHeader(final Tab tab) {
this.tab = tab;
}
public TabOrHeader(@StringRes final int header) {
this.header = header;
}
boolean isHeader() {
return header != 0;
}
}
public interface TabAdapterCallback {
void onStartDrag(TabViewHolder viewHolder);
void onOrderChange(List newOrderTabs);
void onAdd(Tab tab);
void onRemove(Tab tab);
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/UserSearchResultsAdapter.java
================================================
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import awais.instagrabber.adapters.DirectUsersAdapter.OnDirectUserClickListener;
import awais.instagrabber.adapters.viewholder.directmessages.DirectUserViewHolder;
import awais.instagrabber.adapters.viewholder.directmessages.RecipientThreadViewHolder;
import awais.instagrabber.databinding.LayoutDmUserItemBinding;
import awais.instagrabber.repositories.responses.directmessages.RankedRecipient;
public final class UserSearchResultsAdapter extends ListAdapter {
private static final int VIEW_TYPE_USER = 0;
private static final int VIEW_TYPE_THREAD = 1;
private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() {
@Override
public boolean areItemsTheSame(@NonNull final RankedRecipient oldItem, @NonNull final RankedRecipient newItem) {
final boolean bothUsers = oldItem.getUser() != null && newItem.getUser() != null;
if (!bothUsers) return false;
final boolean bothThreads = oldItem.getThread() != null && newItem.getThread() != null;
if (!bothThreads) return false;
if (bothUsers) {
return oldItem.getUser().getPk() == newItem.getUser().getPk();
}
return Objects.equals(oldItem.getThread().getThreadId(), newItem.getThread().getThreadId());
}
@Override
public boolean areContentsTheSame(@NonNull final RankedRecipient oldItem, @NonNull final RankedRecipient newItem) {
final boolean bothUsers = oldItem.getUser() != null && newItem.getUser() != null;
if (bothUsers) {
return Objects.equals(oldItem.getUser().getUsername(), newItem.getUser().getUsername()) &&
Objects.equals(oldItem.getUser().getFullName(), newItem.getUser().getFullName());
}
return Objects.equals(oldItem.getThread().getThreadTitle(), newItem.getThread().getThreadTitle());
}
};
private final boolean showSelection;
private final Set selectedRecipients;
private final OnDirectUserClickListener onUserClickListener;
private final OnRecipientClickListener onRecipientClickListener;
public UserSearchResultsAdapter(final boolean showSelection,
final OnRecipientClickListener onRecipientClickListener) {
super(DIFF_CALLBACK);
this.showSelection = showSelection;
selectedRecipients = showSelection ? new HashSet<>() : null;
this.onRecipientClickListener = onRecipientClickListener;
this.onUserClickListener = (position, user, selected) -> {
if (onRecipientClickListener != null) {
onRecipientClickListener.onClick(position, RankedRecipient.of(user), selected);
}
};
setHasStableIds(true);
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final LayoutDmUserItemBinding binding = LayoutDmUserItemBinding.inflate(layoutInflater, parent, false);
if (viewType == VIEW_TYPE_USER) {
return new DirectUserViewHolder(binding, onUserClickListener, null);
}
return new RecipientThreadViewHolder(binding, onRecipientClickListener);
}
@Override
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position) {
final RankedRecipient recipient = getItem(position);
final int itemViewType = getItemViewType(position);
if (itemViewType == VIEW_TYPE_USER) {
boolean isSelected = false;
if (selectedRecipients != null) {
isSelected = selectedRecipients.stream()
.anyMatch(rankedRecipient -> rankedRecipient.getUser() != null
&& rankedRecipient.getUser().getPk() == recipient.getUser().getPk());
}
((DirectUserViewHolder) holder).bind(position, recipient.getUser(), false, false, showSelection, isSelected);
return;
}
boolean isSelected = false;
if (selectedRecipients != null) {
isSelected = selectedRecipients.stream()
.anyMatch(rankedRecipient -> rankedRecipient.getThread() != null
&& Objects.equals(rankedRecipient.getThread().getThreadId(), recipient.getThread().getThreadId()));
}
((RecipientThreadViewHolder) holder).bind(position, recipient.getThread(), showSelection, isSelected);
}
@Override
public long getItemId(final int position) {
final RankedRecipient recipient = getItem(position);
if (recipient.getUser() != null) {
return recipient.getUser().getPk();
}
if (recipient.getThread() != null) {
return recipient.getThread().getThreadTitle().hashCode();
}
return 0;
}
@Override
public int getItemViewType(final int position) {
final RankedRecipient recipient = getItem(position);
return recipient.getUser() != null ? VIEW_TYPE_USER : VIEW_TYPE_THREAD;
}
public void setSelectedRecipient(final RankedRecipient recipient, final boolean selected) {
if (selectedRecipients == null || recipient == null || (recipient.getUser() == null && recipient.getThread() == null)) return;
final boolean isUser = recipient.getUser() != null;
int position = -1;
final List currentList = getCurrentList();
for (int i = 0; i < currentList.size(); i++) {
final RankedRecipient temp = currentList.get(i);
if (isUser) {
if (temp.getUser() != null && temp.getUser().getPk() == recipient.getUser().getPk()) {
position = i;
break;
}
continue;
}
if (temp.getThread() != null && Objects.equals(temp.getThread().getThreadId(), recipient.getThread().getThreadId())) {
position = i;
break;
}
}
if (position < 0) return;
if (selected) {
selectedRecipients.add(recipient);
} else {
selectedRecipients.remove(recipient);
}
notifyItemChanged(position);
}
public interface OnRecipientClickListener {
void onClick(int position, RankedRecipient recipient, final boolean isSelected);
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/CommentViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder;
import android.content.Context;
import android.content.res.Resources;
import android.util.TypedValue;
import android.view.Menu;
import android.view.View;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.appcompat.view.ContextThemeWrapper;
import androidx.appcompat.widget.PopupMenu;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.R;
import awais.instagrabber.adapters.CommentsAdapter.CommentCallback;
import awais.instagrabber.customviews.ProfilePicView;
import awais.instagrabber.databinding.ItemCommentBinding;
import awais.instagrabber.models.Comment;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.utils.Utils;
public final class CommentViewHolder extends RecyclerView.ViewHolder {
private final ItemCommentBinding binding;
private final long currentUserId;
private final CommentCallback commentCallback;
@ColorInt
private int parentCommentHighlightColor;
private PopupMenu optionsPopup;
public CommentViewHolder(@NonNull final ItemCommentBinding binding,
final long currentUserId,
final CommentCallback commentCallback) {
super(binding.getRoot());
this.binding = binding;
this.currentUserId = currentUserId;
this.commentCallback = commentCallback;
final Context context = itemView.getContext();
if (context == null) return;
final Resources.Theme theme = context.getTheme();
if (theme == null) return;
final TypedValue typedValue = new TypedValue();
final boolean resolved = theme.resolveAttribute(R.attr.parentCommentHighlightColor, typedValue, true);
if (resolved) {
parentCommentHighlightColor = typedValue.data;
}
}
public void bind(final Comment comment, final boolean isReplyParent, final boolean isReply) {
if (comment == null) return;
itemView.setOnClickListener(v -> {
if (commentCallback != null) {
commentCallback.onClick(comment);
}
});
if (isReplyParent && parentCommentHighlightColor != 0) {
itemView.setBackgroundColor(parentCommentHighlightColor);
} else {
itemView.setBackgroundColor(itemView.getResources().getColor(android.R.color.transparent));
}
setupCommentText(comment, isReply);
binding.date.setText(comment.getDateTime());
setLikes(comment, isReply);
setReplies(comment, isReply);
setUser(comment, isReply);
setupOptions(comment, isReply);
}
private void setupCommentText(@NonNull final Comment comment, final boolean isReply) {
binding.comment.clearOnURLClickListeners();
binding.comment.clearOnHashtagClickListeners();
binding.comment.clearOnMentionClickListeners();
binding.comment.clearOnEmailClickListeners();
binding.comment.setText(comment.getText());
binding.comment.setTextSize(TypedValue.COMPLEX_UNIT_SP, isReply ? 12 : 14);
binding.comment.addOnHashtagListener(autoLinkItem -> {
final String originalText = autoLinkItem.getOriginalText();
if (commentCallback == null) return;
commentCallback.onHashtagClick(originalText);
});
binding.comment.addOnMentionClickListener(autoLinkItem -> {
final String originalText = autoLinkItem.getOriginalText();
if (commentCallback == null) return;
commentCallback.onMentionClick(originalText);
});
binding.comment.addOnEmailClickListener(autoLinkItem -> {
final String originalText = autoLinkItem.getOriginalText();
if (commentCallback == null) return;
commentCallback.onEmailClick(originalText);
});
binding.comment.addOnURLClickListener(autoLinkItem -> {
final String originalText = autoLinkItem.getOriginalText();
if (commentCallback == null) return;
commentCallback.onURLClick(originalText);
});
binding.comment.setOnLongClickListener(v -> {
Utils.copyText(itemView.getContext(), comment.getText());
return true;
});
binding.comment.setOnClickListener(v -> commentCallback.onClick(comment));
}
private void setUser(@NonNull final Comment comment, final boolean isReply) {
final User user = comment.getUser();
if (user == null) return;
binding.username.setUsername(user.getUsername(), user.isVerified());
binding.username.setTextAppearance(itemView.getContext(), isReply ? R.style.TextAppearance_MaterialComponents_Subtitle2
: R.style.TextAppearance_MaterialComponents_Subtitle1);
binding.username.setOnClickListener(v -> {
if (commentCallback == null) return;
commentCallback.onMentionClick("@" + user.getUsername());
});
binding.profilePic.setImageURI(user.getProfilePicUrl());
binding.profilePic.setSize(isReply ? ProfilePicView.Size.SMALLER : ProfilePicView.Size.SMALL);
binding.profilePic.setOnClickListener(v -> {
if (commentCallback == null) return;
commentCallback.onMentionClick("@" + user.getUsername());
});
}
private void setLikes(@NonNull final Comment comment, final boolean isReply) {
binding.likes.setText(String.valueOf(comment.getCommentLikeCount()));
binding.likes.setOnLongClickListener(v -> {
if (commentCallback == null) return false;
commentCallback.onViewLikes(comment);
return true;
});
if (currentUserId == 0) { // not logged in
binding.likes.setOnClickListener(v -> {
if (commentCallback == null) return;
commentCallback.onViewLikes(comment);
});
return;
}
final boolean liked = comment.getLiked();
final int resId = liked ? R.drawable.ic_like : R.drawable.ic_not_liked;
binding.likes.setCompoundDrawablesRelativeWithSize(ContextCompat.getDrawable(itemView.getContext(), resId), null, null, null);
binding.likes.setOnClickListener(v -> {
if (commentCallback == null) return;
// toggle like
commentCallback.onLikeClick(comment, !liked, isReply);
});
}
private void setReplies(@NonNull final Comment comment, final boolean isReply) {
final int replies = comment.getChildCommentCount();
binding.replies.setVisibility(View.VISIBLE);
final String text = isReply ? "" : String.valueOf(replies);
binding.replies.setText(text);
binding.replies.setOnClickListener(v -> {
if (commentCallback == null) return;
commentCallback.onRepliesClick(comment);
});
}
private void setupOptions(final Comment comment, final boolean isReply) {
binding.options.setOnClickListener(v -> {
if (optionsPopup == null) {
createOptionsPopupMenu(comment, isReply);
}
if (optionsPopup == null) return;
optionsPopup.show();
});
}
private void createOptionsPopupMenu(final Comment comment, final boolean isReply) {
if (optionsPopup == null) {
final ContextThemeWrapper themeWrapper = new ContextThemeWrapper(itemView.getContext(), R.style.popupMenuStyle);
optionsPopup = new PopupMenu(themeWrapper, binding.options);
} else {
optionsPopup.getMenu().clear();
}
optionsPopup.getMenuInflater().inflate(R.menu.comment_options_menu, optionsPopup.getMenu());
final User user = comment.getUser();
if (currentUserId == 0 || user == null || user.getPk() != currentUserId) {
final Menu menu = optionsPopup.getMenu();
menu.removeItem(R.id.delete);
}
optionsPopup.setOnMenuItemClickListener(item -> {
if (commentCallback == null) return false;
int itemId = item.getItemId();
if (itemId == R.id.translate) {
commentCallback.onTranslate(comment);
return true;
}
if (itemId == R.id.delete) {
commentCallback.onDelete(comment, isReply);
}
return true;
});
}
// private void setupReply(final Comment comment) {
// if (!isLoggedIn) {
// binding.reply.setVisibility(View.GONE);
// return;
// }
// binding.reply.setOnClickListener(v -> {
// if (commentCallback == null) return;
// // toggle like
// commentCallback.onReplyClick(comment);
// });
// }
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/DiscoverViewHolder.java
================================================
//package awais.instagrabber.adapters.viewholder;
//
//import android.view.View;
//import android.widget.ImageView;
//
//import androidx.annotation.NonNull;
//import androidx.recyclerview.widget.RecyclerView;
//
//import com.facebook.drawee.view.SimpleDraweeView;
//
//import awais.instagrabber.R;
//
//public final class DiscoverViewHolder extends RecyclerView.ViewHolder {
// public final SimpleDraweeView postImage;
// public final ImageView typeIcon;
// public final View selectedView;
// // public final View progressView;
//
// public DiscoverViewHolder(@NonNull final View itemView) {
// super(itemView);
// typeIcon = itemView.findViewById(R.id.typeIcon);
// postImage = itemView.findViewById(R.id.postImage);
// selectedView = itemView.findViewById(R.id.selectedView);
// // progressView = itemView.findViewById(R.id.progressView);
// }
//}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/FavoriteViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.adapters.FavoritesAdapter;
import awais.instagrabber.databinding.ItemSearchResultBinding;
import awais.instagrabber.db.entities.Favorite;
import awais.instagrabber.models.enums.FavoriteType;
import awais.instagrabber.utils.Constants;
public class FavoriteViewHolder extends RecyclerView.ViewHolder {
private static final String TAG = "FavoriteViewHolder";
private final ItemSearchResultBinding binding;
public FavoriteViewHolder(@NonNull final ItemSearchResultBinding binding) {
super(binding.getRoot());
this.binding = binding;
binding.verified.setVisibility(View.GONE);
}
public void bind(final Favorite model,
final FavoritesAdapter.OnFavoriteClickListener clickListener,
final FavoritesAdapter.OnFavoriteLongClickListener longClickListener) {
// Log.d(TAG, "bind: " + model);
if (model == null) return;
itemView.setOnClickListener(v -> {
if (clickListener == null) return;
clickListener.onClick(model);
});
itemView.setOnLongClickListener(v -> {
if (clickListener == null) return false;
return longClickListener.onLongClick(model);
});
if (model.getType() == FavoriteType.HASHTAG) {
binding.profilePic.setImageURI(Constants.DEFAULT_HASH_TAG_PIC);
} else {
binding.profilePic.setImageURI(model.getPicUrl());
}
binding.title.setVisibility(View.VISIBLE);
binding.subtitle.setText(model.getDisplayName());
String query = model.getQuery();
switch (model.getType()) {
case HASHTAG:
query = "#" + query;
break;
case USER:
query = "@" + query;
break;
case LOCATION:
binding.title.setVisibility(View.GONE);
break;
default:
// do nothing
}
binding.title.setText(query);
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/FeedGridItemViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder;
import android.content.Context;
import android.content.res.ColorStateList;
import android.net.Uri;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.DimenRes;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.drawee.backends.pipeline.PipelineDraweeControllerBuilder;
import com.facebook.imagepipeline.common.ResizeOptions;
import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.imagepipeline.request.ImageRequestBuilder;
import java.util.List;
import awais.instagrabber.R;
import awais.instagrabber.adapters.FeedAdapterV2;
import awais.instagrabber.databinding.ItemFeedGridBinding;
import awais.instagrabber.models.PostsLayoutPreferences;
import awais.instagrabber.models.enums.MediaItemType;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.DownloadUtils;
import awais.instagrabber.utils.ResponseBodyUtils;
import awais.instagrabber.utils.TextUtils;
import static awais.instagrabber.models.PostsLayoutPreferences.PostsLayoutType.STAGGERED_GRID;
public class FeedGridItemViewHolder extends RecyclerView.ViewHolder {
private final ItemFeedGridBinding binding;
public FeedGridItemViewHolder(@NonNull final ItemFeedGridBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
public void bind(final int position,
@NonNull final Media media,
@NonNull final PostsLayoutPreferences layoutPreferences,
final FeedAdapterV2.FeedItemCallback feedItemCallback,
final FeedAdapterV2.AdapterSelectionCallback adapterSelectionCallback,
final boolean selectionModeActive,
final boolean selected) {
itemView.setOnClickListener(v -> {
if (!selectionModeActive && feedItemCallback != null) {
feedItemCallback.onPostClick(media);
return;
}
if (selectionModeActive && adapterSelectionCallback != null) {
adapterSelectionCallback.onPostClick(position, media);
}
});
if (adapterSelectionCallback != null) {
itemView.setOnLongClickListener(v -> adapterSelectionCallback.onPostLongClick(position, media));
}
binding.selectedView.setVisibility(selected ? View.VISIBLE : View.GONE);
// for rounded borders (clip view to background shape)
itemView.setClipToOutline(layoutPreferences.getHasRoundedCorners());
if (layoutPreferences.getType() == STAGGERED_GRID) {
final float aspectRatio = (float) media.getOriginalWidth() / media.getOriginalHeight();
binding.postImage.setAspectRatio(aspectRatio);
} else {
binding.postImage.setAspectRatio(1);
}
setUserDetails(media, layoutPreferences);
String thumbnailUrl = null;
final int typeIconRes;
final MediaItemType mediaType = media.getType();
if (mediaType == null) return;
switch (mediaType) {
case MEDIA_TYPE_IMAGE:
typeIconRes = -1;
thumbnailUrl = ResponseBodyUtils.getThumbUrl(media);
break;
case MEDIA_TYPE_VIDEO:
thumbnailUrl = ResponseBodyUtils.getThumbUrl(media);
typeIconRes = R.drawable.exo_icon_play;
break;
case MEDIA_TYPE_SLIDER:
final List sliderItems = media.getCarouselMedia();
if (sliderItems != null) {
final Media child = sliderItems.get(0);
if (child != null) {
thumbnailUrl = ResponseBodyUtils.getThumbUrl(child);
if (layoutPreferences.getType() == STAGGERED_GRID) {
final float childAspectRatio = (float) child.getOriginalWidth() / child.getOriginalHeight();
binding.postImage.setAspectRatio(childAspectRatio);
}
}
}
typeIconRes = R.drawable.ic_checkbox_multiple_blank_stroke;
break;
default:
typeIconRes = -1;
thumbnailUrl = null;
}
setThumbImage(thumbnailUrl);
if (typeIconRes <= 0) {
binding.typeIcon.setVisibility(View.GONE);
} else {
binding.typeIcon.setVisibility(View.VISIBLE);
binding.typeIcon.setImageResource(typeIconRes);
}
binding.downloaded.setVisibility(View.GONE);
final Context context = itemView.getContext();
if (context == null) {
return;
}
AppExecutors.INSTANCE.getTasksThread().execute(() -> {
final List checkList = DownloadUtils.checkDownloaded(media, context);
if (checkList.isEmpty()) {
return;
}
AppExecutors.INSTANCE.getMainThread().execute(() -> {
switch (media.getType()) {
case MEDIA_TYPE_IMAGE:
case MEDIA_TYPE_VIDEO:
binding.downloaded.setVisibility(checkList.get(0) ? View.VISIBLE : View.GONE);
binding.downloaded.setImageTintList(ColorStateList.valueOf(itemView.getResources().getColor(R.color.green_A400)));
break;
case MEDIA_TYPE_SLIDER:
binding.downloaded.setVisibility(checkList.get(0) ? View.VISIBLE : View.GONE);
final List carouselMedia = media.getCarouselMedia();
boolean allDownloaded = checkList.size() == (carouselMedia == null ? 0 : carouselMedia.size());
if (allDownloaded) {
allDownloaded = checkList.stream().allMatch(downloaded -> downloaded);
}
binding.downloaded.setImageTintList(ColorStateList.valueOf(itemView.getResources().getColor(
allDownloaded ? R.color.green_A400 : R.color.yellow_400)));
break;
default:
}
});
});
}
private void setThumbImage(final String thumbnailUrl) {
if (TextUtils.isEmpty(thumbnailUrl)) {
binding.postImage.setController(null);
return;
}
final ImageRequest requestBuilder = ImageRequestBuilder.newBuilderWithSource(Uri.parse(thumbnailUrl))
.setResizeOptions(ResizeOptions.forDimensions(binding.postImage.getWidth(),
binding.postImage.getHeight()))
.setLocalThumbnailPreviewsEnabled(true)
.setProgressiveRenderingEnabled(true)
.build();
final PipelineDraweeControllerBuilder builder = Fresco.newDraweeControllerBuilder()
.setImageRequest(requestBuilder)
.setOldController(binding.postImage.getController());
binding.postImage.setController(builder.build());
}
private void setUserDetails(@NonNull final Media media,
@NonNull final PostsLayoutPreferences layoutPreferences) {
final User user = media.getUser();
if (layoutPreferences.isAvatarVisible()) {
if (user == null) {
binding.profilePic.setVisibility(View.GONE);
} else {
final String profilePicUrl = user.getProfilePicUrl();
if (TextUtils.isEmpty(profilePicUrl)) {
binding.profilePic.setVisibility(View.GONE);
} else {
binding.profilePic.setVisibility(View.VISIBLE);
binding.profilePic.setImageURI(profilePicUrl);
}
}
final ViewGroup.LayoutParams layoutParams = binding.profilePic.getLayoutParams();
@DimenRes final int dimenRes;
switch (layoutPreferences.getProfilePicSize()) {
case SMALL:
dimenRes = R.dimen.profile_pic_size_small;
break;
case TINY:
dimenRes = R.dimen.profile_pic_size_tiny;
break;
default:
case REGULAR:
dimenRes = R.dimen.profile_pic_size_regular;
break;
}
final int dimensionPixelSize = itemView.getResources().getDimensionPixelSize(dimenRes);
layoutParams.width = dimensionPixelSize;
layoutParams.height = dimensionPixelSize;
binding.profilePic.requestLayout();
} else {
binding.profilePic.setVisibility(View.GONE);
}
if (layoutPreferences.isNameVisible()) {
if (user == null) {
binding.name.setVisibility(View.GONE);
} else {
final String username = user.getUsername();
if (username == null) {
binding.name.setVisibility(View.GONE);
} else {
binding.name.setVisibility(View.VISIBLE);
binding.name.setText(username);
}
}
} else {
binding.name.setVisibility(View.GONE);
}
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/FeedStoryViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.adapters.FeedStoriesAdapter;
import awais.instagrabber.databinding.ItemHighlightBinding;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.stories.Story;
public final class FeedStoryViewHolder extends RecyclerView.ViewHolder {
private final ItemHighlightBinding binding;
public FeedStoryViewHolder(final ItemHighlightBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
public void bind(final Story model,
final int position,
final FeedStoriesAdapter.OnFeedStoryClickListener listener) {
if (model == null) return;
binding.getRoot().setOnClickListener(v -> {
if (listener == null) return;
listener.onFeedStoryClick(model, position);
});
binding.getRoot().setOnLongClickListener(v -> {
if (listener != null) listener.onFeedStoryLongClick(model, position);
return true;
});
final User profileModel = model.getUser();
binding.title.setText(profileModel.getUsername());
final boolean isFullyRead =
model.getSeen() != null &&
model.getSeen().equals(model.getLatestReelMedia());
binding.title.setAlpha(isFullyRead ? 0.5F : 1.0F);
binding.icon.setImageURI(profileModel.getProfilePicUrl());
binding.icon.setAlpha(isFullyRead ? 0.5F : 1.0F);
if (model.getBroadcast() != null) binding.icon.setStoriesBorder(2);
else if (model.getHasBestiesMedia() == true) binding.icon.setStoriesBorder(1);
else binding.icon.setStoriesBorder(0);
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/FilterViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder;
import android.graphics.Bitmap;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.google.common.collect.ImmutableList;
import java.util.Collection;
import awais.instagrabber.adapters.FiltersAdapter;
import awais.instagrabber.databinding.ItemFilterBinding;
import awais.instagrabber.fragments.imageedit.filters.filters.Filter;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.BitmapUtils;
import jp.co.cyberagent.android.gpuimage.GPUImage;
import jp.co.cyberagent.android.gpuimage.filter.GPUImageFilter;
public class FilterViewHolder extends RecyclerView.ViewHolder {
private static final String TAG = FilterViewHolder.class.getSimpleName();
private final ItemFilterBinding binding;
private final Collection tuneFilters;
private final FiltersAdapter.OnFilterClickListener onFilterClickListener;
private final AppExecutors appExecutors;
public FilterViewHolder(@NonNull final ItemFilterBinding binding,
final Collection tuneFilters,
final FiltersAdapter.OnFilterClickListener onFilterClickListener) {
super(binding.getRoot());
this.binding = binding;
this.tuneFilters = tuneFilters;
this.onFilterClickListener = onFilterClickListener;
appExecutors = AppExecutors.INSTANCE;
}
public void bind(final int position, final String originalKey, final Bitmap originalBitmap, final Filter> item, final boolean isSelected) {
if (originalBitmap == null || item == null) return;
if (onFilterClickListener != null) {
itemView.setOnClickListener(v -> onFilterClickListener.onClick(position, item));
}
if (item.getLabel() != -1) {
binding.name.setVisibility(View.VISIBLE);
binding.name.setText(item.getLabel());
binding.name.setSelected(isSelected);
} else {
binding.name.setVisibility(View.GONE);
}
final String filterKey = item.getLabel() + "_" + originalKey;
// avoid resetting the bitmap
if (binding.preview.getTag() != null && binding.preview.getTag().equals(filterKey)) return;
binding.preview.setTag(filterKey);
final Bitmap bitmap = BitmapUtils.getBitmapFromMemCache(filterKey);
if (bitmap == null) {
final GPUImageFilter filter = item.getInstance();
appExecutors.getTasksThread().submit(() -> {
GPUImage.getBitmapForMultipleFilters(
originalBitmap,
ImmutableList.builder().add(filter).addAll(tuneFilters).build(),
filteredBitmap -> {
BitmapUtils.addBitmapToMemoryCache(filterKey, filteredBitmap, true);
appExecutors.getMainThread().execute(() -> binding.getRoot().post(() -> binding.preview.setImageBitmap(filteredBitmap)));
}
);
});
return;
}
binding.getRoot().post(() -> binding.preview.setImageBitmap(bitmap));
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/FollowsViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.databinding.ItemFollowBinding;
import awais.instagrabber.repositories.responses.User;
public final class FollowsViewHolder extends RecyclerView.ViewHolder {
private final ItemFollowBinding binding;
public FollowsViewHolder(@NonNull final ItemFollowBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
public void bind(final User model,
final View.OnClickListener onClickListener) {
if (model == null) return;
itemView.setTag(model);
itemView.setOnClickListener(onClickListener);
binding.username.setUsername("@" + model.getUsername(), model.isVerified());
binding.fullName.setText(model.getFullName());
binding.profilePic.setImageURI(model.getProfilePicUrl());
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/HighlightViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.databinding.ItemHighlightBinding;
import awais.instagrabber.repositories.responses.stories.Story;
public final class HighlightViewHolder extends RecyclerView.ViewHolder {
private final ItemHighlightBinding binding;
public HighlightViewHolder(final ItemHighlightBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
public void bind(final Story model) {
if (model == null) return;
binding.title.setText(model.getTitle());
binding.icon.setImageURI(model.getCoverMedia().getCroppedImageVersion().getUrl());
// binding.getRoot().setOnClickListener(v -> {
// if (listener == null) return;
// listener.onFeedStoryClick(model, position);
// });
// final ProfileModel profileModel = model.getProfileModel();
// binding.title.setText(profileModel.getUsername());
// binding.title.setAlpha(model.getFullyRead() ? 0.5F : 1.0F);
// binding.icon.setImageURI(profileModel.getSdProfilePic());
// binding.icon.setAlpha(model.getFullyRead() ? 0.5F : 1.0F);
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/NotificationViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder;
import android.text.TextUtils;
import android.view.View;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.R;
import awais.instagrabber.adapters.NotificationsAdapter.OnNotificationClickListener;
import awais.instagrabber.databinding.ItemNotificationBinding;
import awais.instagrabber.models.enums.NotificationType;
import awais.instagrabber.repositories.responses.notification.Notification;
import awais.instagrabber.repositories.responses.notification.NotificationArgs;
public final class NotificationViewHolder extends RecyclerView.ViewHolder {
private final ItemNotificationBinding binding;
public NotificationViewHolder(final ItemNotificationBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
public void bind(final Notification model,
final OnNotificationClickListener notificationClickListener) {
if (model == null) return;
int text = -1;
CharSequence subtext = null;
final NotificationArgs args = model.getArgs();
switch (model.getType()) {
case LIKE:
text = R.string.liked_notif;
break;
case COMMENT: // untested
text = R.string.comment_notif;
subtext = args.getText();
break;
case TAGGED:
text = R.string.tagged_notif;
break;
case FOLLOW:
text = R.string.follow_notif;
break;
case REQUEST:
text = R.string.request_notif;
break;
case COMMENT_MENTION:
case COMMENT_LIKE:
case TAGGED_COMMENT:
case RESPONDED_STORY:
subtext = args.getText();
break;
case AYML:
subtext = args.getFullName();
break;
}
binding.tvSubComment.setText(model.getType() == NotificationType.AYML ? args.getText() : subtext);
if (text == -1 && subtext != null) {
binding.tvComment.setText(args.getText());
binding.tvComment.setVisibility(TextUtils.isEmpty(args.getText()) || args.getText().equals(args.getFullName())
? View.GONE : View.VISIBLE);
binding.tvSubComment.setText(subtext);
binding.tvSubComment.setVisibility(model.getType() == NotificationType.AYML ? View.VISIBLE : View.GONE);
} else if (text != -1) {
binding.tvComment.setText(text);
binding.tvSubComment.setVisibility(subtext == null ? View.GONE : View.VISIBLE);
}
binding.tvDate.setVisibility(model.getType() == NotificationType.AYML ? View.GONE : View.VISIBLE);
if (model.getType() != NotificationType.AYML) {
binding.tvDate.setText(args.getDateTime());
}
binding.isVerified.setVisibility(args.isVerified() ? View.VISIBLE : View.GONE);
binding.tvUsername.setText(args.getUsername());
binding.ivProfilePic.setImageURI(args.getProfilePic());
binding.ivProfilePic.setOnClickListener(v -> {
if (notificationClickListener == null) return;
notificationClickListener.onProfileClick(args.getUsername());
});
if (model.getType() == NotificationType.AYML) {
binding.ivPreviewPic.setVisibility(View.GONE);
} else if (args.getMedia() == null) {
binding.ivPreviewPic.setVisibility(View.INVISIBLE);
} else {
binding.ivPreviewPic.setVisibility(View.VISIBLE);
binding.ivPreviewPic.setImageURI(args.getMedia().get(0).getImage());
binding.ivPreviewPic.setOnClickListener(v -> {
if (notificationClickListener == null) return;
notificationClickListener.onPreviewClick(model);
});
}
itemView.setOnClickListener(v -> {
if (notificationClickListener == null) return;
notificationClickListener.onNotificationClick(model);
});
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/SearchItemViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.R;
import awais.instagrabber.databinding.ItemSearchResultBinding;
import awais.instagrabber.fragments.search.SearchCategoryFragment.OnSearchItemClickListener;
import awais.instagrabber.models.enums.FavoriteType;
import awais.instagrabber.repositories.responses.Hashtag;
import awais.instagrabber.repositories.responses.Place;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.search.SearchItem;
public class SearchItemViewHolder extends RecyclerView.ViewHolder {
private final ItemSearchResultBinding binding;
private final OnSearchItemClickListener onSearchItemClickListener;
public SearchItemViewHolder(@NonNull final ItemSearchResultBinding binding,
final OnSearchItemClickListener onSearchItemClickListener) {
super(binding.getRoot());
this.binding = binding;
this.onSearchItemClickListener = onSearchItemClickListener;
}
public void bind(final SearchItem searchItem) {
if (searchItem == null) return;
final FavoriteType type = searchItem.getType();
if (type == null) return;
String title;
String subtitle;
String picUrl;
boolean isVerified = false;
switch (type) {
case USER:
final User user = searchItem.getUser();
title = "@" + user.getUsername();
subtitle = user.getFullName();
picUrl = user.getProfilePicUrl();
isVerified = user.isVerified();
break;
case HASHTAG:
final Hashtag hashtag = searchItem.getHashtag();
title = "#" + hashtag.getName();
subtitle = hashtag.getSearchResultSubtitle();
picUrl = "res:/" + R.drawable.ic_hashtag;
break;
case LOCATION:
final Place place = searchItem.getPlace();
title = place.getTitle();
subtitle = place.getSubtitle();
picUrl = "res:/" + R.drawable.ic_location;
break;
default:
return;
}
itemView.setOnClickListener(v -> {
if (onSearchItemClickListener != null) {
onSearchItemClickListener.onSearchItemClick(searchItem);
}
});
binding.delete.setVisibility(searchItem.isRecent() ? View.VISIBLE : View.GONE);
if (searchItem.isRecent()) {
binding.delete.setEnabled(true);
binding.delete.setOnClickListener(v -> {
if (onSearchItemClickListener != null) {
binding.delete.setEnabled(false);
onSearchItemClickListener.onSearchItemDelete(searchItem, type);
}
});
}
binding.title.setText(title);
binding.subtitle.setText(subtitle);
binding.profilePic.setImageURI(picUrl);
binding.verified.setVisibility(isVerified ? View.VISIBLE : View.GONE);
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/SliderItemViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.adapters.SliderItemsAdapter;
import awais.instagrabber.repositories.responses.Media;
public abstract class SliderItemViewHolder extends RecyclerView.ViewHolder {
private static final String TAG = "FeedSliderItemViewHolder";
public SliderItemViewHolder(@NonNull final View itemView) {
super(itemView);
}
public abstract void bind(final Media media,
final int position,
final SliderItemsAdapter.SliderCallback sliderCallback);
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/SliderPhotoViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder;
import android.graphics.drawable.Animatable;
import android.net.Uri;
import android.view.MotionEvent;
import androidx.annotation.NonNull;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.drawee.controller.BaseControllerListener;
import com.facebook.imagepipeline.image.ImageInfo;
import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.imagepipeline.request.ImageRequestBuilder;
import awais.instagrabber.adapters.SliderItemsAdapter;
import awais.instagrabber.customviews.drawee.AnimatedZoomableController;
import awais.instagrabber.customviews.drawee.DoubleTapGestureListener;
import awais.instagrabber.databinding.ItemSliderPhotoBinding;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.utils.ResponseBodyUtils;
public class SliderPhotoViewHolder extends SliderItemViewHolder {
private static final String TAG = "FeedSliderPhotoViewHolder";
private final ItemSliderPhotoBinding binding;
public SliderPhotoViewHolder(@NonNull final ItemSliderPhotoBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
public void bind(@NonNull final Media model,
final int position,
final SliderItemsAdapter.SliderCallback sliderCallback) {
final ImageRequest requestBuilder = ImageRequestBuilder
.newBuilderWithSource(Uri.parse(ResponseBodyUtils.getImageUrl(model)))
.setLocalThumbnailPreviewsEnabled(true)
.build();
binding.getRoot()
.setController(Fresco.newDraweeControllerBuilder()
.setImageRequest(requestBuilder)
.setControllerListener(new BaseControllerListener() {
@Override
public void onFailure(final String id, final Throwable throwable) {
if (sliderCallback != null) {
sliderCallback.onThumbnailLoaded(position);
}
}
@Override
public void onFinalImageSet(final String id,
final ImageInfo imageInfo,
final Animatable animatable) {
if (sliderCallback != null) {
sliderCallback.onThumbnailLoaded(position);
}
}
})
.setLowResImageRequest(ImageRequest.fromUri(ResponseBodyUtils.getThumbUrl(model)))
.build());
final DoubleTapGestureListener tapListener = new DoubleTapGestureListener(binding.getRoot()) {
@Override
public boolean onSingleTapConfirmed(final MotionEvent e) {
if (sliderCallback != null) {
sliderCallback.onItemClicked(position, model, binding.getRoot());
}
return super.onSingleTapConfirmed(e);
}
};
binding.getRoot().setTapListener(tapListener);
final AnimatedZoomableController zoomableController = AnimatedZoomableController.newInstance();
zoomableController.setMaxScaleFactor(3f);
binding.getRoot().setZoomableController(zoomableController);
binding.getRoot().setZoomingEnabled(true);
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/SliderVideoViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder;
import android.annotation.SuppressLint;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import com.google.android.exoplayer2.ui.StyledPlayerView;
import java.util.List;
import awais.instagrabber.adapters.SliderItemsAdapter;
import awais.instagrabber.customviews.VideoPlayerCallbackAdapter;
import awais.instagrabber.customviews.VideoPlayerViewHelper;
import awais.instagrabber.databinding.LayoutVideoPlayerWithThumbnailBinding;
import awais.instagrabber.fragments.settings.PreferenceKeys;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.MediaCandidate;
import awais.instagrabber.utils.NumberUtils;
import awais.instagrabber.utils.ResponseBodyUtils;
import awais.instagrabber.utils.Utils;
import static awais.instagrabber.utils.Utils.settingsHelper;
public class SliderVideoViewHolder extends SliderItemViewHolder {
private static final String TAG = "SliderVideoViewHolder";
private final LayoutVideoPlayerWithThumbnailBinding binding;
private final boolean loadVideoOnItemClick;
private VideoPlayerViewHelper videoPlayerViewHelper;
@SuppressLint("ClickableViewAccessibility")
public SliderVideoViewHolder(@NonNull final LayoutVideoPlayerWithThumbnailBinding binding,
final boolean loadVideoOnItemClick) {
super(binding.getRoot());
this.binding = binding;
this.loadVideoOnItemClick = loadVideoOnItemClick;
final GestureDetector.OnGestureListener videoPlayerViewGestureListener = new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onSingleTapConfirmed(final MotionEvent e) {
binding.playerView.performClick();
return true;
}
};
final GestureDetector gestureDetector = new GestureDetector(itemView.getContext(), videoPlayerViewGestureListener);
binding.playerView.setOnTouchListener((v, event) -> {
gestureDetector.onTouchEvent(event);
return true;
});
}
public void bind(@NonNull final Media media,
final int position,
final SliderItemsAdapter.SliderCallback sliderCallback) {
final float vol = settingsHelper.getBoolean(PreferenceKeys.MUTED_VIDEOS) ? 0f : 1f;
final VideoPlayerViewHelper.VideoPlayerCallback videoPlayerCallback = new VideoPlayerCallbackAdapter() {
@Override
public void onThumbnailClick() {
if (sliderCallback != null) {
sliderCallback.onItemClicked(position, media, binding.getRoot());
}
}
@Override
public void onThumbnailLoaded() {
if (sliderCallback != null) {
sliderCallback.onThumbnailLoaded(position);
}
}
@Override
public void onPlayerViewLoaded() {
// binding.itemFeedBottom.btnMute.setVisibility(View.VISIBLE);
final ViewGroup.LayoutParams layoutParams = binding.playerView.getLayoutParams();
final int requiredWidth = Utils.displayMetrics.widthPixels;
final int resultingHeight = NumberUtils.getResultingHeight(requiredWidth, media.getOriginalHeight(), media.getOriginalWidth());
layoutParams.width = requiredWidth;
layoutParams.height = resultingHeight;
binding.playerView.requestLayout();
// setMuteIcon(vol == 0f && Utils.sessionVolumeFull ? 1f : vol);
}
@Override
public void onPlay() {
if (sliderCallback != null) {
sliderCallback.onPlayerPlay(position);
}
}
@Override
public void onPause() {
if (sliderCallback != null) {
sliderCallback.onPlayerPause(position);
}
}
@Override
public void onRelease() {
if (sliderCallback != null) {
sliderCallback.onPlayerRelease(position);
}
}
@Override
public void onFullScreenModeChanged(final boolean isFullScreen, final StyledPlayerView playerView) {
if (sliderCallback != null) {
sliderCallback.onFullScreenModeChanged(isFullScreen, playerView);
}
}
@Override
public boolean isInFullScreen() {
if (sliderCallback != null) {
return sliderCallback.isInFullScreen();
}
return false;
}
};
final float aspectRatio = (float) media.getOriginalWidth() / media.getOriginalHeight();
String videoUrl = null;
final List videoVersions = media.getVideoVersions();
if (videoVersions != null && !videoVersions.isEmpty()) {
final MediaCandidate videoVersion = videoVersions.get(0);
if (videoVersion != null) {
videoUrl = videoVersion.getUrl();
}
}
if (videoUrl == null) return;
videoPlayerViewHelper = new VideoPlayerViewHelper(binding.getRoot().getContext(),
binding,
videoUrl,
vol,
aspectRatio,
ResponseBodyUtils.getThumbUrl(media),
loadVideoOnItemClick,
videoPlayerCallback);
binding.playerView.setOnClickListener(v -> {
if (sliderCallback != null) {
sliderCallback.onItemClicked(position, media, binding.getRoot());
}
});
}
public void pause() {
if (videoPlayerViewHelper == null) return;
videoPlayerViewHelper.pause();
}
public void releasePlayer() {
if (videoPlayerViewHelper == null) return;
videoPlayerViewHelper.releasePlayer();
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/StoryListViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder;
import android.view.View;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.R;
import awais.instagrabber.adapters.FeedStoriesListAdapter.OnFeedStoryClickListener;
import awais.instagrabber.adapters.HighlightStoriesListAdapter.OnHighlightStoryClickListener;
import awais.instagrabber.databinding.ItemNotificationBinding;
import awais.instagrabber.repositories.responses.stories.Story;
import awais.instagrabber.utils.ResponseBodyUtils;
public final class StoryListViewHolder extends RecyclerView.ViewHolder {
private final ItemNotificationBinding binding;
public StoryListViewHolder(final ItemNotificationBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
public void bind(final Story model,
final OnFeedStoryClickListener notificationClickListener) {
if (model == null) return;
final int storiesCount = model.getMediaCount();
binding.tvComment.setVisibility(View.VISIBLE);
binding.tvComment.setText(itemView.getResources().getQuantityString(R.plurals.stories_count, storiesCount, storiesCount));
binding.tvSubComment.setVisibility(View.GONE);
binding.tvDate.setText(model.getDateTime());
binding.tvUsername.setText(model.getUser().getUsername());
binding.ivProfilePic.setImageURI(model.getUser().getProfilePicUrl());
binding.ivProfilePic.setOnClickListener(v -> {
if (notificationClickListener == null) return;
notificationClickListener.onProfileClick(model.getUser().getUsername());
});
if (model.getItems() != null && model.getItems().size() > 0) {
binding.ivPreviewPic.setVisibility(View.VISIBLE);
binding.ivPreviewPic.setImageURI(ResponseBodyUtils.getThumbUrl(model.getItems().get(0)));
} else binding.ivPreviewPic.setVisibility(View.INVISIBLE);
float alpha = model.getSeen() != null && model.getSeen().equals(model.getLatestReelMedia())
? 0.5F : 1.0F;
binding.ivProfilePic.setAlpha(alpha);
binding.ivPreviewPic.setAlpha(alpha);
binding.tvUsername.setAlpha(alpha);
binding.tvComment.setAlpha(alpha);
binding.tvDate.setAlpha(alpha);
itemView.setOnClickListener(v -> {
if (notificationClickListener == null) return;
notificationClickListener.onFeedStoryClick(model);
});
}
public void bind(final Story model,
final int position,
final OnHighlightStoryClickListener notificationClickListener) {
if (model == null) return;
final int storiesCount = model.getMediaCount();
binding.tvComment.setVisibility(View.VISIBLE);
binding.tvComment.setText(itemView.getResources().getQuantityString(R.plurals.stories_count, storiesCount, storiesCount));
binding.tvSubComment.setVisibility(View.GONE);
binding.tvUsername.setText(model.getDateTime());
binding.ivProfilePic.setVisibility(View.GONE);
binding.ivPreviewPic.setVisibility(View.VISIBLE);
binding.ivPreviewPic.setImageURI(model.getCoverImageVersion().getUrl());
itemView.setOnClickListener(v -> {
if (notificationClickListener == null) return;
notificationClickListener.onHighlightClick(model, position);
});
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/TabViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder;
import android.annotation.SuppressLint;
import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable;
import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.core.widget.ImageViewCompat;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.color.MaterialColors;
import awais.instagrabber.R;
import awais.instagrabber.adapters.TabsAdapter;
import awais.instagrabber.databinding.ItemTabOrderPrefBinding;
import awais.instagrabber.models.Tab;
public class TabViewHolder extends RecyclerView.ViewHolder {
private final ItemTabOrderPrefBinding binding;
private final TabsAdapter.TabAdapterCallback tabAdapterCallback;
private final int highlightColor;
private final Drawable originalBgColor;
private boolean draggable = true;
@SuppressLint("ClickableViewAccessibility")
public TabViewHolder(@NonNull final ItemTabOrderPrefBinding binding,
@NonNull final TabsAdapter.TabAdapterCallback tabAdapterCallback) {
super(binding.getRoot());
this.binding = binding;
this.tabAdapterCallback = tabAdapterCallback;
highlightColor = MaterialColors.getColor(itemView.getContext(), R.attr.colorControlHighlight, 0);
originalBgColor = itemView.getBackground();
binding.handle.setOnTouchListener((v, event) -> {
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
tabAdapterCallback.onStartDrag(this);
}
return true;
});
}
public void bind(@NonNull final Tab tab,
final boolean isInOthers,
final boolean isCurrentFull) {
draggable = !isInOthers;
binding.icon.setImageResource(tab.getIconResId());
binding.title.setText(tab.getTitle());
binding.handle.setVisibility(isInOthers ? View.GONE : View.VISIBLE);
binding.addRemove.setImageResource(isInOthers ? R.drawable.ic_round_add_circle_24
: R.drawable.ic_round_remove_circle_24);
final ColorStateList tintList = ColorStateList.valueOf(ContextCompat.getColor(
itemView.getContext(),
isInOthers ? R.color.green_500
: R.color.red_500));
ImageViewCompat.setImageTintList(binding.addRemove, tintList);
binding.addRemove.setOnClickListener(v -> {
if (isInOthers) {
tabAdapterCallback.onAdd(tab);
return;
}
tabAdapterCallback.onRemove(tab);
});
final boolean enabled = tab.isRemovable()
&& !(isInOthers && isCurrentFull); // All slots are full in current
binding.addRemove.setEnabled(enabled);
binding.addRemove.setAlpha(enabled ? 1 : 0.5F);
}
public boolean isDraggable() {
return draggable;
}
public void setDragging(final boolean isDragging) {
if (isDragging) {
if (highlightColor != 0) {
itemView.setBackgroundColor(highlightColor);
} else {
itemView.setAlpha(0.5F);
}
return;
}
itemView.setAlpha(1);
itemView.setBackground(originalBgColor);
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/TopicClusterViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.GradientDrawable;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.palette.graphics.Palette;
import androidx.recyclerview.widget.RecyclerView;
import com.facebook.common.executors.CallerThreadExecutor;
import com.facebook.common.references.CloseableReference;
import com.facebook.datasource.DataSource;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.imagepipeline.core.ImagePipeline;
import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber;
import com.facebook.imagepipeline.image.CloseableImage;
import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.imagepipeline.request.ImageRequestBuilder;
import java.util.concurrent.atomic.AtomicInteger;
import awais.instagrabber.R;
import awais.instagrabber.adapters.DiscoverTopicsAdapter;
import awais.instagrabber.adapters.SavedCollectionsAdapter;
import awais.instagrabber.databinding.ItemDiscoverTopicBinding;
import awais.instagrabber.repositories.responses.discover.TopicCluster;
import awais.instagrabber.repositories.responses.saved.SavedCollection;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.utils.ResponseBodyUtils;
public class TopicClusterViewHolder extends RecyclerView.ViewHolder {
private final ItemDiscoverTopicBinding binding;
private final DiscoverTopicsAdapter.OnTopicClickListener onTopicClickListener;
private final SavedCollectionsAdapter.OnCollectionClickListener onCollectionClickListener;
public TopicClusterViewHolder(@NonNull final ItemDiscoverTopicBinding binding,
final DiscoverTopicsAdapter.OnTopicClickListener onTopicClickListener,
final SavedCollectionsAdapter.OnCollectionClickListener onCollectionClickListener) {
super(binding.getRoot());
this.binding = binding;
this.onTopicClickListener = onTopicClickListener;
this.onCollectionClickListener = onCollectionClickListener;
}
public void bind(final TopicCluster topicCluster) {
if (topicCluster == null) {
return;
}
final AtomicInteger titleColor = new AtomicInteger(-1);
final AtomicInteger backgroundColor = new AtomicInteger(-1);
if (onTopicClickListener != null) {
itemView.setOnClickListener(v -> onTopicClickListener.onTopicClick(
topicCluster,
binding.cover,
titleColor.get(),
backgroundColor.get()
));
itemView.setOnLongClickListener(v -> {
onTopicClickListener.onTopicLongClick(topicCluster.getCoverMedia());
return true;
});
}
// binding.title.setTransitionName("title-" + topicCluster.getId());
binding.cover.setTransitionName("cover-" + topicCluster.getId());
final String thumbUrl = ResponseBodyUtils.getThumbUrl(topicCluster.getCoverMedia());
if (thumbUrl == null) {
binding.cover.setImageURI((String) null);
} else {
final ImageRequest imageRequest = ImageRequestBuilder
.newBuilderWithSource(Uri.parse(thumbUrl))
.build();
final ImagePipeline imagePipeline = Fresco.getImagePipeline();
final DataSource> dataSource = imagePipeline
.fetchDecodedImage(imageRequest, CallerThreadExecutor.getInstance());
dataSource.subscribe(new BaseBitmapDataSubscriber() {
@Override
public void onNewResultImpl(@Nullable Bitmap bitmap) {
if (dataSource.isFinished()) {
dataSource.close();
}
if (bitmap != null) {
Palette.from(bitmap).generate(p -> {
final Resources resources = itemView.getResources();
int titleTextColor = resources.getColor(R.color.white);
if (p != null) {
final Palette.Swatch swatch = p.getDominantSwatch();
if (swatch != null) {
backgroundColor.set(swatch.getRgb());
GradientDrawable gd = new GradientDrawable(
GradientDrawable.Orientation.TOP_BOTTOM,
new int[]{Color.TRANSPARENT, backgroundColor.get()});
titleTextColor = swatch.getTitleTextColor();
binding.background.setBackground(gd);
}
}
titleColor.set(titleTextColor);
binding.title.setTextColor(titleTextColor);
});
}
}
@Override
public void onFailureImpl(@NonNull DataSource dataSource) {
dataSource.close();
}
}, CallerThreadExecutor.getInstance());
binding.cover.setImageRequest(imageRequest);
}
binding.title.setText(topicCluster.getTitle());
}
public void bind(final SavedCollection topicCluster) {
if (topicCluster == null) {
return;
}
final AtomicInteger titleColor = new AtomicInteger(-1);
final AtomicInteger backgroundColor = new AtomicInteger(-1);
if (onCollectionClickListener != null) {
itemView.setOnClickListener(v -> onCollectionClickListener.onCollectionClick(
topicCluster,
binding.getRoot(),
binding.cover,
binding.title,
titleColor.get(),
backgroundColor.get()
));
}
// binding.title.setTransitionName("title-" + topicCluster.getCollectionId());
binding.cover.setTransitionName("cover-" + topicCluster.getCollectionId());
final Media coverMedia = topicCluster.getCoverMediaList() == null
? topicCluster.getCoverMedia()
: topicCluster.getCoverMediaList().get(0);
final String thumbUrl = ResponseBodyUtils.getThumbUrl(coverMedia);
if (thumbUrl == null) {
binding.cover.setImageURI((String) null);
} else {
final ImageRequest imageRequest = ImageRequestBuilder
.newBuilderWithSource(Uri.parse(thumbUrl))
.build();
final ImagePipeline imagePipeline = Fresco.getImagePipeline();
final DataSource> dataSource = imagePipeline
.fetchDecodedImage(imageRequest, CallerThreadExecutor.getInstance());
dataSource.subscribe(new BaseBitmapDataSubscriber() {
@Override
public void onNewResultImpl(@Nullable Bitmap bitmap) {
if (dataSource.isFinished()) {
dataSource.close();
}
if (bitmap != null) {
Palette.from(bitmap).generate(p -> {
final Resources resources = itemView.getResources();
int titleTextColor = resources.getColor(R.color.white);
if (p != null) {
final Palette.Swatch swatch = p.getDominantSwatch();
if (swatch != null) {
backgroundColor.set(swatch.getRgb());
GradientDrawable gd = new GradientDrawable(
GradientDrawable.Orientation.TOP_BOTTOM,
new int[]{Color.TRANSPARENT, backgroundColor.get()}
);
titleTextColor = swatch.getTitleTextColor();
binding.background.setBackground(gd);
}
}
titleColor.set(titleTextColor);
binding.title.setTextColor(titleTextColor);
});
}
}
@Override
public void onFailureImpl(@NonNull DataSource dataSource) {
dataSource.close();
}
}, CallerThreadExecutor.getInstance());
binding.cover.setImageRequest(imageRequest);
}
binding.title.setText(topicCluster.getCollectionName());
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/dialogs/KeywordsFilterDialogViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder.dialogs;
import android.content.Context;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.HashSet;
import awais.instagrabber.R;
import awais.instagrabber.adapters.KeywordsFilterAdapter;
import awais.instagrabber.fragments.settings.PreferenceKeys;
import awais.instagrabber.utils.SettingsHelper;
public class KeywordsFilterDialogViewHolder extends RecyclerView.ViewHolder {
private final Button deleteButton;
private final TextView item;
public KeywordsFilterDialogViewHolder(@NonNull View itemView) {
super(itemView);
deleteButton = itemView.findViewById(R.id.keyword_delete);
item = itemView.findViewById(R.id.keyword_text);
}
public void bind(ArrayList items, int position, Context context, KeywordsFilterAdapter adapter){
item.setText(items.get(position));
deleteButton.setOnClickListener(view -> {
final String s = items.get(position);
SettingsHelper settingsHelper = new SettingsHelper(context);
items.remove(position);
settingsHelper.putStringSet(PreferenceKeys.KEYWORD_FILTERS, new HashSet<>(items));
adapter.notifyDataSetChanged();
final String message = context.getString(R.string.removed_keywords, s);
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
});
}
public Button getDeleteButton(){
return deleteButton;
}
public TextView getTextView(){
return item;
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectInboxItemViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder.directmessages;
import android.content.res.Resources;
import android.graphics.Typeface;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.ConstraintSet;
import androidx.recyclerview.widget.RecyclerView;
import com.facebook.drawee.view.SimpleDraweeView;
import com.google.common.collect.ImmutableList;
import java.util.List;
import awais.instagrabber.R;
import awais.instagrabber.adapters.DirectMessageInboxAdapter.OnItemClickListener;
import awais.instagrabber.databinding.LayoutDmInboxItemBinding;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
import awais.instagrabber.utils.DMUtils;
import awais.instagrabber.utils.TextUtils;
public final class DirectInboxItemViewHolder extends RecyclerView.ViewHolder {
// private static final String TAG = "DMInboxItemVH";
private final LayoutDmInboxItemBinding binding;
private final OnItemClickListener onClickListener;
private final List multipleProfilePics;
private final int childSmallSize;
private final int childTinySize;
public DirectInboxItemViewHolder(@NonNull final LayoutDmInboxItemBinding binding,
final OnItemClickListener onClickListener) {
super(binding.getRoot());
this.binding = binding;
this.onClickListener = onClickListener;
multipleProfilePics = ImmutableList.of(
binding.multiPic1,
binding.multiPic2,
binding.multiPic3
);
childSmallSize = itemView.getResources().getDimensionPixelSize(R.dimen.dm_inbox_avatar_size_small);
childTinySize = itemView.getResources().getDimensionPixelSize(R.dimen.dm_inbox_avatar_size_tiny);
}
public void bind(final DirectThread thread) {
if (thread == null) return;
if (onClickListener != null) {
itemView.setOnClickListener((v) -> onClickListener.onItemClick(thread));
}
setProfilePics(thread);
setTitle(thread);
final List items = thread.getItems();
if (items == null || items.isEmpty()) return;
final DirectItem item = thread.getFirstDirectItem();
if (item == null) return;
setDateTime(item);
setSubtitle(thread);
setReadState(thread);
}
private void setProfilePics(@NonNull final DirectThread thread) {
final List users = thread.getUsers();
if (users.size() > 1) {
binding.profilePic.setVisibility(View.GONE);
binding.multiPicContainer.setVisibility(View.VISIBLE);
for (int i = 0; i < Math.min(3, users.size()); ++i) {
final User user = users.get(i);
final SimpleDraweeView view = multipleProfilePics.get(i);
view.setVisibility(user == null ? View.GONE : View.VISIBLE);
if (user == null) return;
final String profilePicUrl = user.getProfilePicUrl();
view.setImageURI(profilePicUrl);
setChildSize(view, users.size());
if (i == 1) {
updateConstraints(view, users.size());
}
view.requestLayout();
}
return;
}
binding.profilePic.setVisibility(View.VISIBLE);
binding.multiPicContainer.setVisibility(View.GONE);
final String profilePicUrl = users.size() == 1 ? users.get(0).getProfilePicUrl() : null;
if (profilePicUrl == null) {
binding.profilePic.setController(null);
return;
}
binding.profilePic.setImageURI(profilePicUrl);
}
private void updateConstraints(final SimpleDraweeView view, final int length) {
final ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) view.getLayoutParams();
if (length >= 2) {
layoutParams.endToEnd = ConstraintSet.PARENT_ID;
layoutParams.bottomToBottom = ConstraintSet.PARENT_ID;
}
if (length == 3) {
layoutParams.startToStart = ConstraintSet.PARENT_ID;
layoutParams.topToTop = ConstraintSet.PARENT_ID;
}
}
private void setChildSize(final SimpleDraweeView view, final int length) {
final int size = length == 3 ? childTinySize : childSmallSize;
final ConstraintLayout.LayoutParams viewLayoutParams = new ConstraintLayout.LayoutParams(size, size);
view.setLayoutParams(viewLayoutParams);
}
private void setTitle(@NonNull final DirectThread thread) {
final String threadTitle = thread.getThreadTitle();
binding.threadTitle.setText(threadTitle);
}
private void setSubtitle(@NonNull final DirectThread thread) {
final Resources resources = itemView.getResources();
final long viewerId = thread.getViewerId();
// final DirectThreadDirectStory directStory = thread.getDirectStory();
// if (directStory != null && !directStory.getItems().isEmpty()) {
// final DirectItem item = directStory.getItems().get(0);
// final MediaItemType mediaType = item.getVisualMedia().getMedia().getMediaType();
// final String username = DMUtils.getUsername(thread.getUsers(), item.getUserId(), viewerId, resources);
// final String subtitle = DMUtils.getMediaSpecificSubtitle(username, resources, mediaType);
// binding.subtitle.setText(subtitle);
// return;
// }
final DirectItem item = thread.getFirstDirectItem();
if (item == null) return;
final String subtitle = DMUtils.getMessageString(thread, resources, viewerId, item);
binding.subtitle.setText(subtitle != null ? subtitle : "");
}
private void setDateTime(@NonNull final DirectItem item) {
final long timestamp = item.getTimestamp() / 1000;
final String dateTimeString = TextUtils.getRelativeDateTimeString(timestamp);
binding.tvDate.setText(dateTimeString);
}
private void setReadState(@NonNull final DirectThread thread) {
final boolean read = DMUtils.isRead(thread);
binding.unread.setVisibility(read ? View.GONE : View.VISIBLE);
binding.threadTitle.setTypeface(null, read ? Typeface.NORMAL : Typeface.BOLD);
binding.subtitle.setTypeface(null, read ? Typeface.NORMAL : Typeface.BOLD);
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemActionLogViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder.directmessages;
import android.graphics.Typeface;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.ItemTouchHelper;
import java.util.List;
import awais.instagrabber.R;
import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemCallback;
import awais.instagrabber.databinding.LayoutDmActionLogBinding;
import awais.instagrabber.databinding.LayoutDmBaseBinding;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectItemActionLog;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
import awais.instagrabber.repositories.responses.directmessages.TextRange;
import awais.instagrabber.utils.TextUtils;
public class DirectItemActionLogViewHolder extends DirectItemViewHolder {
private static final String TAG = DirectItemActionLogViewHolder.class.getSimpleName();
private final LayoutDmActionLogBinding binding;
public DirectItemActionLogViewHolder(@NonNull final LayoutDmBaseBinding baseBinding,
final LayoutDmActionLogBinding binding,
final User currentUser,
final DirectThread thread,
final DirectItemCallback callback) {
super(baseBinding, currentUser, thread, callback);
this.binding = binding;
setItemView(binding.getRoot());
binding.tvMessage.setMovementMethod(LinkMovementMethod.getInstance());
}
@Override
public void bindItem(final DirectItem directItemModel, final MessageDirection messageDirection) {
final DirectItemActionLog actionLog = directItemModel.getActionLog();
final String text = actionLog.getDescription();
final SpannableStringBuilder sb = new SpannableStringBuilder(text);
final List bold = actionLog.getBold();
if (bold != null && !bold.isEmpty()) {
for (final TextRange textRange : bold) {
final StyleSpan boldStyleSpan = new StyleSpan(Typeface.BOLD);
sb.setSpan(boldStyleSpan, textRange.getStart(), textRange.getEnd(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
}
}
final List textAttributes = actionLog.getTextAttributes();
if (textAttributes != null && !textAttributes.isEmpty()) {
for (final TextRange textAttribute : textAttributes) {
if (!TextUtils.isEmpty(textAttribute.getColor())) {
final ForegroundColorSpan colorSpan = new ForegroundColorSpan(itemView.getResources().getColor(R.color.deep_orange_400));
sb.setSpan(colorSpan, textAttribute.getStart(), textAttribute.getEnd(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
}
if (!TextUtils.isEmpty(textAttribute.getIntent())) {
final ClickableSpan clickableSpan = new ClickableSpan() {
@Override
public void onClick(@NonNull final View widget) {
handleDeepLink(textAttribute.getIntent());
}
};
sb.setSpan(clickableSpan, textAttribute.getStart(), textAttribute.getEnd(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
}
}
}
binding.tvMessage.setText(sb);
}
@Override
protected boolean allowMessageDirectionGravity() {
return false;
}
@Override
protected boolean showUserDetailsInGroup() {
return false;
}
@Override
protected boolean showMessageInfo() {
return false;
}
@Override
protected boolean allowLongClick() {
return false;
}
@Override
public int getSwipeDirection() {
return ItemTouchHelper.ACTION_STATE_IDLE;
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemAnimatedMediaViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder.directmessages;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.ItemTouchHelper;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.google.common.collect.ImmutableList;
import java.util.List;
import awais.instagrabber.R;
import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemCallback;
import awais.instagrabber.customviews.DirectItemContextMenu;
import awais.instagrabber.databinding.LayoutDmAnimatedMediaBinding;
import awais.instagrabber.databinding.LayoutDmBaseBinding;
import awais.instagrabber.repositories.responses.AnimatedMediaFixedHeight;
import awais.instagrabber.repositories.responses.AnimatedMediaImages;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectItemAnimatedMedia;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
import awais.instagrabber.utils.NullSafePair;
import awais.instagrabber.utils.NumberUtils;
import awais.instagrabber.utils.Utils;
public class DirectItemAnimatedMediaViewHolder extends DirectItemViewHolder {
private final LayoutDmAnimatedMediaBinding binding;
public DirectItemAnimatedMediaViewHolder(@NonNull final LayoutDmBaseBinding baseBinding,
@NonNull final LayoutDmAnimatedMediaBinding binding,
final User currentUser,
final DirectThread thread,
final DirectItemCallback callback) {
super(baseBinding, currentUser, thread, callback);
this.binding = binding;
setItemView(binding.getRoot());
}
@Override
public void bindItem(final DirectItem item, final MessageDirection messageDirection) {
final DirectItemAnimatedMedia animatedMediaModel = item.getAnimatedMedia();
final AnimatedMediaImages images = animatedMediaModel.getImages();
if (images == null) return;
final AnimatedMediaFixedHeight fixedHeight = images.getFixedHeight();
if (fixedHeight == null) return;
final String url = fixedHeight.getWebp();
final NullSafePair widthHeight = NumberUtils.calculateWidthHeight(
fixedHeight.getHeight(),
fixedHeight.getWidth(),
mediaImageMaxHeight,
mediaImageMaxWidth
);
binding.ivAnimatedMessage.setVisibility(View.VISIBLE);
final ViewGroup.LayoutParams layoutParams = binding.ivAnimatedMessage.getLayoutParams();
final int width = widthHeight.first;
final int height = widthHeight.second;
layoutParams.width = width;
layoutParams.height = height;
binding.ivAnimatedMessage.requestLayout();
binding.ivAnimatedMessage.setController(Fresco.newDraweeControllerBuilder()
.setUri(url)
.setAutoPlayAnimations(true)
.build());
}
@Override
public int getSwipeDirection() {
return ItemTouchHelper.ACTION_STATE_IDLE;
}
@Override
protected List getLongClickOptions() {
return ImmutableList.of(
new DirectItemContextMenu.MenuItem(R.id.detail, R.string.dms_inbox_giphy, item -> {
Utils.openURL(itemView.getContext(), "https://giphy.com/gifs/" + item.getAnimatedMedia().getId());
return null;
})
);
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemDefaultViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder.directmessages;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.ItemTouchHelper;
import awais.instagrabber.R;
import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemCallback;
import awais.instagrabber.databinding.LayoutDmBaseBinding;
import awais.instagrabber.databinding.LayoutDmTextBinding;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
public class DirectItemDefaultViewHolder extends DirectItemViewHolder {
private final LayoutDmTextBinding binding;
public DirectItemDefaultViewHolder(@NonNull final LayoutDmBaseBinding baseBinding,
@NonNull final LayoutDmTextBinding binding,
final User currentUser,
final DirectThread thread,
final DirectItemCallback callback) {
super(baseBinding, currentUser, thread, callback);
this.binding = binding;
setItemView(binding.getRoot());
}
@Override
public void bindItem(final DirectItem directItemModel, final MessageDirection messageDirection) {
final Context context = itemView.getContext();
binding.tvMessage.setText(context.getText(R.string.dms_inbox_raven_message_unknown));
}
@Override
protected boolean showBackground() {
return true;
}
@Override
protected boolean allowLongClick() {
return false;
}
@Override
public int getSwipeDirection() {
return ItemTouchHelper.ACTION_STATE_IDLE;
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemLikeViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder.directmessages;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.ItemTouchHelper;
import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemCallback;
import awais.instagrabber.databinding.LayoutDmBaseBinding;
import awais.instagrabber.databinding.LayoutDmLikeBinding;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
public class DirectItemLikeViewHolder extends DirectItemViewHolder {
public DirectItemLikeViewHolder(@NonNull final LayoutDmBaseBinding baseBinding,
@NonNull final LayoutDmLikeBinding binding,
final User currentUser,
final DirectThread thread,
final DirectItemCallback callback) {
super(baseBinding, currentUser, thread, callback);
setItemView(binding.getRoot());
}
@Override
public void bindItem(final DirectItem directItemModel, final MessageDirection messageDirection) {}
@Override
protected boolean canForward() {
return false;
}
@Override
public int getSwipeDirection() {
return ItemTouchHelper.ACTION_STATE_IDLE;
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemLinkViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder.directmessages;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import com.google.common.collect.ImmutableList;
import java.util.List;
import awais.instagrabber.R;
import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemCallback;
import awais.instagrabber.customviews.DirectItemContextMenu;
import awais.instagrabber.databinding.LayoutDmBaseBinding;
import awais.instagrabber.databinding.LayoutDmLinkBinding;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectItemLink;
import awais.instagrabber.repositories.responses.directmessages.DirectItemLinkContext;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils;
public class DirectItemLinkViewHolder extends DirectItemViewHolder {
private final LayoutDmLinkBinding binding;
public DirectItemLinkViewHolder(@NonNull final LayoutDmBaseBinding baseBinding,
final LayoutDmLinkBinding binding,
final User currentUser,
final DirectThread thread,
final DirectItemCallback callback) {
super(baseBinding, currentUser, thread, callback);
this.binding = binding;
final int width = windowWidth - margin - dmRadiusSmall;
final ViewGroup.LayoutParams layoutParams = binding.preview.getLayoutParams();
layoutParams.width = width;
binding.preview.requestLayout();
setItemView(binding.getRoot());
}
@Override
public void bindItem(final DirectItem item, final MessageDirection messageDirection) {
final DirectItemLink link = item.getLink();
if (link == null) return;
final DirectItemLinkContext linkContext = link.getLinkContext();
if (linkContext == null) return;
final String linkImageUrl = linkContext.getLinkImageUrl();
if (TextUtils.isEmpty(linkImageUrl)) {
binding.preview.setVisibility(View.GONE);
} else {
binding.preview.setVisibility(View.VISIBLE);
binding.preview.setImageURI(linkImageUrl);
}
if (TextUtils.isEmpty(linkContext.getLinkTitle())) {
binding.title.setVisibility(View.GONE);
} else {
binding.title.setVisibility(View.VISIBLE);
binding.title.setText(linkContext.getLinkTitle());
}
if (TextUtils.isEmpty(linkContext.getLinkSummary())) {
binding.summary.setVisibility(View.GONE);
} else {
binding.summary.setVisibility(View.VISIBLE);
binding.summary.setText(linkContext.getLinkSummary());
}
if (TextUtils.isEmpty(linkContext.getLinkUrl())) {
binding.url.setVisibility(View.GONE);
} else {
binding.url.setVisibility(View.VISIBLE);
binding.url.setText(linkContext.getLinkUrl());
}
binding.text.setText(link.getText());
setupListeners(linkContext);
}
private void setupListeners(final DirectItemLinkContext linkContext) {
setupRamboTextListeners(binding.text);
final View.OnClickListener onClickListener = v -> openURL(linkContext.getLinkUrl());
binding.preview.setOnClickListener(onClickListener);
// binding.preview.setOnLongClickListener(v -> itemView.performLongClick());
binding.title.setOnClickListener(onClickListener);
binding.summary.setOnClickListener(onClickListener);
binding.url.setOnClickListener(onClickListener);
}
@Override
protected boolean showBackground() {
return true;
}
@Override
protected List getLongClickOptions() {
return ImmutableList.of(
new DirectItemContextMenu.MenuItem(R.id.copy, R.string.copy, item -> {
final DirectItemLink link = item.getLink();
if (link == null || TextUtils.isEmpty(link.getText())) return null;
Utils.copyText(itemView.getContext(), link.getText());
return null;
})
);
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemMediaShareViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder.directmessages;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.ItemTouchHelper;
import com.facebook.drawee.drawable.ScalingUtils;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
import com.facebook.drawee.generic.RoundingParams;
import com.google.common.collect.ImmutableList;
import java.util.List;
import java.util.Objects;
import awais.instagrabber.R;
import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemCallback;
import awais.instagrabber.customviews.DirectItemContextMenu;
import awais.instagrabber.databinding.LayoutDmBaseBinding;
import awais.instagrabber.databinding.LayoutDmMediaShareBinding;
import awais.instagrabber.models.enums.DirectItemType;
import awais.instagrabber.models.enums.MediaItemType;
import awais.instagrabber.repositories.responses.Caption;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectItemClip;
import awais.instagrabber.repositories.responses.directmessages.DirectItemFelixShare;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
import awais.instagrabber.utils.NullSafePair;
import awais.instagrabber.utils.NumberUtils;
import awais.instagrabber.utils.ResponseBodyUtils;
import awais.instagrabber.utils.Utils;
public class DirectItemMediaShareViewHolder extends DirectItemViewHolder {
private static final String TAG = DirectItemMediaShareViewHolder.class.getSimpleName();
private final LayoutDmMediaShareBinding binding;
private final RoundingParams incomingRoundingParams;
private final RoundingParams outgoingRoundingParams;
private DirectItemType itemType;
private Caption caption;
public DirectItemMediaShareViewHolder(@NonNull final LayoutDmBaseBinding baseBinding,
@NonNull final LayoutDmMediaShareBinding binding,
final User currentUser,
final DirectThread thread,
final DirectItemCallback callback) {
super(baseBinding, currentUser, thread, callback);
this.binding = binding;
incomingRoundingParams = RoundingParams.fromCornersRadii(dmRadiusSmall, dmRadius, dmRadius, dmRadius);
outgoingRoundingParams = RoundingParams.fromCornersRadii(dmRadius, dmRadiusSmall, dmRadius, dmRadius);
setItemView(binding.getRoot());
}
@Override
public void bindItem(final DirectItem item, final MessageDirection messageDirection) {
binding.topBg.setBackgroundResource(messageDirection == MessageDirection.INCOMING
? R.drawable.bg_media_share_top_incoming
: R.drawable.bg_media_share_top_outgoing);
Media media = getMedia(item);
if (media == null) return;
itemView.post(() -> {
setupUser(media);
setupCaption(media);
});
final int index;
final Media toDisplay;
final MediaItemType mediaType = media.getType();
switch (mediaType) {
case MEDIA_TYPE_SLIDER:
toDisplay = media.getCarouselMedia().stream()
.filter(m -> media.getCarouselShareChildMediaId() != null &&
media.getCarouselShareChildMediaId().equals(m.getId()))
.findAny()
.orElse(media.getCarouselMedia().get(0));
index = media.getCarouselMedia().indexOf(toDisplay);
break;
default:
toDisplay = media;
index = 0;
}
itemView.post(() -> {
setupTypeIndicator(mediaType);
setupPreview(toDisplay, messageDirection);
});
itemView.setOnClickListener(v -> openMedia(media, index));
}
private void setupTypeIndicator(final MediaItemType mediaType) {
final boolean showTypeIcon = mediaType == MediaItemType.MEDIA_TYPE_VIDEO || mediaType == MediaItemType.MEDIA_TYPE_SLIDER;
if (!showTypeIcon) {
binding.typeIcon.setVisibility(View.GONE);
} else {
binding.typeIcon.setVisibility(View.VISIBLE);
binding.typeIcon.setImageResource(mediaType == MediaItemType.MEDIA_TYPE_VIDEO
? R.drawable.ic_video_24
: R.drawable.ic_checkbox_multiple_blank_stroke);
}
}
private void setupPreview(@NonNull final Media media,
final MessageDirection messageDirection) {
final String url = ResponseBodyUtils.getThumbUrl(media);
if (Objects.equals(url, binding.mediaPreview.getTag())) {
return;
}
final RoundingParams roundingParams = messageDirection == MessageDirection.INCOMING ? incomingRoundingParams : outgoingRoundingParams;
binding.mediaPreview.setHierarchy(new GenericDraweeHierarchyBuilder(itemView.getResources())
.setActualImageScaleType(ScalingUtils.ScaleType.CENTER_CROP)
.setRoundingParams(roundingParams)
.build());
final NullSafePair widthHeight = NumberUtils.calculateWidthHeight(
media.getOriginalHeight(),
media.getOriginalWidth(),
mediaImageMaxHeight,
mediaImageMaxWidth
);
final ViewGroup.LayoutParams layoutParams = binding.mediaPreview.getLayoutParams();
layoutParams.width = widthHeight.first;
layoutParams.height = widthHeight.second;
binding.mediaPreview.requestLayout();
binding.mediaPreview.setTag(url);
binding.mediaPreview.setImageURI(url);
}
private void setupCaption(@NonNull final Media media) {
caption = media.getCaption();
if (caption != null) {
binding.caption.setVisibility(View.VISIBLE);
binding.caption.setText(caption.getText());
binding.caption.setEllipsize(TextUtils.TruncateAt.END);
binding.caption.setMaxLines(2);
} else {
binding.caption.setVisibility(View.GONE);
}
}
private void setupUser(@NonNull final Media media) {
final User user = media.getUser();
if (user != null) {
binding.username.setVisibility(View.VISIBLE);
binding.profilePic.setVisibility(View.VISIBLE);
binding.username.setText(user.getUsername());
binding.profilePic.setImageURI(user.getProfilePicUrl());
} else {
binding.username.setVisibility(View.GONE);
binding.profilePic.setVisibility(View.GONE);
}
}
@Nullable
private Media getMedia(@NonNull final DirectItem item) {
Media media = null;
itemType = item.getItemType();
if (itemType == DirectItemType.MEDIA_SHARE) {
media = item.getMediaShare();
} else if (itemType == DirectItemType.CLIP) {
final DirectItemClip clip = item.getClip();
if (clip == null) return null;
media = clip.getClip();
} else if (itemType == DirectItemType.FELIX_SHARE) {
final DirectItemFelixShare felixShare = item.getFelixShare();
if (felixShare == null) return null;
media = felixShare.getVideo();
}
return media;
}
@Override
protected int getReactionsTranslationY() {
return reactionTranslationYType2;
}
@Override
public int getSwipeDirection() {
if (itemType != null && (itemType == DirectItemType.CLIP || itemType == DirectItemType.FELIX_SHARE)) {
return ItemTouchHelper.ACTION_STATE_IDLE;
}
return super.getSwipeDirection();
}
@Override
protected List getLongClickOptions() {
final ImmutableList.Builder builder = ImmutableList.builder();
if (caption != null && !TextUtils.isEmpty(caption.getText())) {
builder.add(new DirectItemContextMenu.MenuItem(R.id.copy, R.string.copy_caption, item -> {
Utils.copyText(itemView.getContext(), caption.getText());
return null;
}));
}
return builder.build();
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemMediaViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder.directmessages;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import com.facebook.drawee.drawable.ScalingUtils;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
import com.facebook.drawee.generic.RoundingParams;
import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemCallback;
import awais.instagrabber.databinding.LayoutDmBaseBinding;
import awais.instagrabber.databinding.LayoutDmMediaBinding;
import awais.instagrabber.models.enums.MediaItemType;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
import awais.instagrabber.utils.NullSafePair;
import awais.instagrabber.utils.NumberUtils;
import awais.instagrabber.utils.ResponseBodyUtils;
public class DirectItemMediaViewHolder extends DirectItemViewHolder {
private final LayoutDmMediaBinding binding;
private final RoundingParams incomingRoundingParams;
private final RoundingParams outgoingRoundingParams;
public DirectItemMediaViewHolder(@NonNull final LayoutDmBaseBinding baseBinding,
@NonNull final LayoutDmMediaBinding binding,
final User currentUser,
final DirectThread thread,
final DirectItemCallback callback) {
super(baseBinding, currentUser, thread, callback);
this.binding = binding;
incomingRoundingParams = RoundingParams.fromCornersRadii(dmRadiusSmall, dmRadius, dmRadius, dmRadius);
outgoingRoundingParams = RoundingParams.fromCornersRadii(dmRadius, dmRadiusSmall, dmRadius, dmRadius);
setItemView(binding.getRoot());
}
@Override
public void bindItem(final DirectItem directItemModel, final MessageDirection messageDirection) {
final RoundingParams roundingParams = messageDirection == MessageDirection.INCOMING ? incomingRoundingParams : outgoingRoundingParams;
binding.mediaPreview.setHierarchy(new GenericDraweeHierarchyBuilder(itemView.getResources())
.setRoundingParams(roundingParams)
.setActualImageScaleType(ScalingUtils.ScaleType.CENTER_CROP)
.build());
final Media media = directItemModel.getMedia();
itemView.setOnClickListener(v -> openMedia(media, -1));
final MediaItemType modelMediaType = media.getType();
binding.typeIcon.setVisibility(modelMediaType == MediaItemType.MEDIA_TYPE_VIDEO || modelMediaType == MediaItemType.MEDIA_TYPE_SLIDER
? View.VISIBLE
: View.GONE);
final NullSafePair widthHeight = NumberUtils.calculateWidthHeight(
media.getOriginalHeight(),
media.getOriginalWidth(),
mediaImageMaxHeight,
mediaImageMaxWidth
);
final ViewGroup.LayoutParams layoutParams = binding.mediaPreview.getLayoutParams();
final int width = widthHeight.first;
layoutParams.width = width;
layoutParams.height = widthHeight.second;
binding.mediaPreview.requestLayout();
binding.bgTime.getLayoutParams().width = width;
binding.bgTime.requestLayout();
final String thumbUrl = ResponseBodyUtils.getThumbUrl(media);
binding.mediaPreview.setImageURI(thumbUrl);
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemPlaceholderViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder.directmessages;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.ItemTouchHelper;
import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemCallback;
import awais.instagrabber.databinding.LayoutDmBaseBinding;
import awais.instagrabber.databinding.LayoutDmStoryShareBinding;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
public class DirectItemPlaceholderViewHolder extends DirectItemViewHolder {
private final LayoutDmStoryShareBinding binding;
public DirectItemPlaceholderViewHolder(@NonNull final LayoutDmBaseBinding baseBinding,
final LayoutDmStoryShareBinding binding,
final User currentUser,
final DirectThread thread,
final DirectItemCallback callback) {
super(baseBinding, currentUser, thread, callback);
this.binding = binding;
setItemView(binding.getRoot());
}
@Override
public void bindItem(final DirectItem directItemModel, final MessageDirection messageDirection) {
binding.shareInfo.setText(directItemModel.getPlaceholder().getTitle());
binding.text.setVisibility(View.VISIBLE);
binding.text.setText(directItemModel.getPlaceholder().getMessage());
binding.ivMediaPreview.setVisibility(View.GONE);
binding.typeIcon.setVisibility(View.GONE);
}
@Override
protected boolean allowLongClick() {
return false;
}
@Override
public int getSwipeDirection() {
return ItemTouchHelper.ACTION_STATE_IDLE;
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemProfileViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder.directmessages;
import android.content.res.Resources;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.ItemTouchHelper;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
import com.facebook.drawee.generic.RoundingParams;
import com.facebook.drawee.view.SimpleDraweeView;
import com.google.common.collect.ImmutableList;
import java.util.List;
import awais.instagrabber.R;
import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemCallback;
import awais.instagrabber.databinding.LayoutDmBaseBinding;
import awais.instagrabber.databinding.LayoutDmProfileBinding;
import awais.instagrabber.models.enums.DirectItemType;
import awais.instagrabber.repositories.responses.Location;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
import awais.instagrabber.utils.ResponseBodyUtils;
import awais.instagrabber.utils.TextUtils;
public class DirectItemProfileViewHolder extends DirectItemViewHolder {
private final LayoutDmProfileBinding binding;
private final ImmutableList previewViews;
public DirectItemProfileViewHolder(@NonNull final LayoutDmBaseBinding baseBinding,
@NonNull final LayoutDmProfileBinding binding,
final User currentUser,
final DirectThread thread,
final DirectItemCallback callback) {
super(baseBinding, currentUser, thread, callback);
this.binding = binding;
setItemView(binding.getRoot());
previewViews = ImmutableList.of(
binding.preview1,
binding.preview2,
binding.preview3,
binding.preview4,
binding.preview5,
binding.preview6
);
}
@Override
public void bindItem(@NonNull final DirectItem item,
final MessageDirection messageDirection) {
binding.getRoot().setBackgroundResource(messageDirection == MessageDirection.INCOMING
? R.drawable.bg_speech_bubble_incoming
: R.drawable.bg_speech_bubble_outgoing);
if (item.getItemType() == DirectItemType.PROFILE) {
setProfile(item);
} else if (item.getItemType() == DirectItemType.LOCATION) {
setLocation(item);
} else {
return;
}
for (final SimpleDraweeView previewView : previewViews) {
previewView.setImageURI((String) null);
}
final List previewMedias = item.getPreviewMedias();
if (previewMedias == null || previewMedias.size() <= 0) {
binding.firstRow.setVisibility(View.GONE);
binding.secondRow.setVisibility(View.GONE);
return;
}
final Resources resources = itemView.getResources();
if (previewMedias.size() <= 3) {
binding.firstRow.setVisibility(View.VISIBLE);
binding.secondRow.setVisibility(View.GONE);
binding.preview1.setHierarchy(new GenericDraweeHierarchyBuilder(resources)
.setRoundingParams(RoundingParams.fromCornersRadii(0, 0, 0, dmRadius))
.build());
binding.preview3.setHierarchy(new GenericDraweeHierarchyBuilder(resources)
.setRoundingParams(RoundingParams.fromCornersRadii(0, 0, dmRadius, 0))
.build());
}
if (previewMedias.size() > 3) {
binding.preview4.setHierarchy(new GenericDraweeHierarchyBuilder(resources)
.setRoundingParams(RoundingParams.fromCornersRadii(0, 0, 0, dmRadius))
.build());
binding.preview6.setHierarchy(new GenericDraweeHierarchyBuilder(resources)
.setRoundingParams(RoundingParams.fromCornersRadii(0, 0, dmRadius, 0))
.build());
}
for (int i = 0; i < previewMedias.size(); i++) {
final Media previewMedia = previewMedias.get(i);
if (previewMedia == null) continue;
final String url = ResponseBodyUtils.getThumbUrl(previewMedia);
if (url == null) continue;
previewViews.get(i).setImageURI(url);
}
}
private void setProfile(@NonNull final DirectItem item) {
final User profile = item.getProfile();
if (profile == null) return;
binding.profilePic.setImageURI(profile.getProfilePicUrl());
binding.username.setText(profile.getUsername());
final String fullName = profile.getFullName();
if (!TextUtils.isEmpty(fullName)) {
binding.fullName.setVisibility(View.VISIBLE);
binding.fullName.setText(fullName);
} else {
binding.fullName.setVisibility(View.GONE);
}
binding.isVerified.setVisibility(profile.isVerified() ? View.VISIBLE : View.GONE);
itemView.setOnClickListener(v -> openProfile(profile.getUsername()));
}
private void setLocation(@NonNull final DirectItem item) {
final Location location = item.getLocation();
if (location == null) return;
binding.profilePic.setVisibility(View.GONE);
binding.username.setText(location.getName());
final String address = location.getAddress();
if (!TextUtils.isEmpty(address)) {
binding.fullName.setText(address);
binding.fullName.setVisibility(View.VISIBLE);
} else {
binding.fullName.setVisibility(View.GONE);
}
binding.isVerified.setVisibility(View.GONE);
itemView.setOnClickListener(v -> openLocation(location.getPk()));
}
@Override
public int getSwipeDirection() {
return ItemTouchHelper.ACTION_STATE_IDLE;
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemRavenMediaViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder.directmessages;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import com.facebook.drawee.drawable.ScalingUtils;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
import com.facebook.drawee.generic.RoundingParams;
import awais.instagrabber.R;
import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemCallback;
import awais.instagrabber.databinding.LayoutDmBaseBinding;
import awais.instagrabber.databinding.LayoutDmRavenMediaBinding;
import awais.instagrabber.models.enums.MediaItemType;
import awais.instagrabber.models.enums.RavenMediaViewMode;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectItemVisualMedia;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
import awais.instagrabber.utils.NullSafePair;
import awais.instagrabber.utils.NumberUtils;
import awais.instagrabber.utils.ResponseBodyUtils;
import awais.instagrabber.utils.TextUtils;
public class DirectItemRavenMediaViewHolder extends DirectItemViewHolder {
private final LayoutDmRavenMediaBinding binding;
private final int maxWidth;
public DirectItemRavenMediaViewHolder(@NonNull final LayoutDmBaseBinding baseBinding,
@NonNull final LayoutDmRavenMediaBinding binding,
final User currentUser,
final DirectThread thread,
final DirectItemCallback callback) {
super(baseBinding, currentUser, thread, callback);
this.binding = binding;
maxWidth = windowWidth - margin - dmRadiusSmall;
setItemView(binding.getRoot());
}
@Override
public void bindItem(final DirectItem directItemModel, final MessageDirection messageDirection) {
final DirectItemVisualMedia visualMedia = directItemModel.getVisualMedia();
final Media media = visualMedia.getMedia();
if (media == null) return;
setExpiryInfo(visualMedia);
setPreview(visualMedia, messageDirection);
final boolean expired = TextUtils.isEmpty(media.getId());
if (expired) return;
itemView.setOnClickListener(v -> openMedia(media, -1));
/*final boolean isExpired = visualMedia == null || (mediaModel = visualMedia.getMedia()) == null ||
TextUtils.isEmpty(mediaModel.getThumbUrl()) && mediaModel.getPk() < 1;
RavenExpiringMediaActionSummary mediaActionSummary = null;
if (visualMedia != null) {
mediaActionSummary = visualMedia.getExpiringMediaActionSummary();
}
binding.mediaExpiredIcon.setVisibility(isExpired ? View.VISIBLE : View.GONE);
int textRes = R.string.dms_inbox_raven_media_unknown;
if (isExpired) textRes = R.string.dms_inbox_raven_media_expired;
if (!isExpired) {
if (mediaActionSummary != null) {
final ActionType expiringMediaType = mediaActionSummary.getType();
if (expiringMediaType == ActionType.DELIVERED)
textRes = R.string.dms_inbox_raven_media_delivered;
else if (expiringMediaType == ActionType.SENT)
textRes = R.string.dms_inbox_raven_media_sent;
else if (expiringMediaType == ActionType.OPENED)
textRes = R.string.dms_inbox_raven_media_opened;
else if (expiringMediaType == ActionType.REPLAYED)
textRes = R.string.dms_inbox_raven_media_replayed;
else if (expiringMediaType == ActionType.SENDING)
textRes = R.string.dms_inbox_raven_media_sending;
else if (expiringMediaType == ActionType.BLOCKED)
textRes = R.string.dms_inbox_raven_media_blocked;
else if (expiringMediaType == ActionType.SUGGESTED)
textRes = R.string.dms_inbox_raven_media_suggested;
else if (expiringMediaType == ActionType.SCREENSHOT)
textRes = R.string.dms_inbox_raven_media_screenshot;
else if (expiringMediaType == ActionType.CANNOT_DELIVER)
textRes = R.string.dms_inbox_raven_media_cant_deliver;
}
final RavenMediaViewMode ravenMediaViewMode = visualMedia.getViewType();
if (ravenMediaViewMode == RavenMediaViewMode.PERMANENT || ravenMediaViewMode == RavenMediaViewMode.REPLAYABLE) {
final MediaItemType mediaType = mediaModel.getMediaType();
textRes = -1;
binding.typeIcon.setVisibility(mediaType == MediaItemType.MEDIA_TYPE_VIDEO || mediaType == MediaItemType.MEDIA_TYPE_SLIDER
? View.VISIBLE
: View.GONE);
final Pair widthHeight = NumberUtils.calculateWidthHeight(
mediaModel.getHeight(),
mediaModel.getWidth(),
maxHeight,
maxWidth
);
final ViewGroup.LayoutParams layoutParams = binding.ivMediaPreview.getLayoutParams();
layoutParams.width = widthHeight.first != null ? widthHeight.first : 0;
layoutParams.height = widthHeight.second != null ? widthHeight.second : 0;
binding.ivMediaPreview.requestLayout();
binding.ivMediaPreview.setImageURI(mediaModel.getThumbUrl());
}
}
if (textRes != -1) {
binding.tvMessage.setText(context.getText(textRes));
binding.tvMessage.setVisibility(View.VISIBLE);
}*/
}
private void setExpiryInfo(final DirectItemVisualMedia visualMedia) {
final Media media = visualMedia.getMedia();
final RavenMediaViewMode viewMode = visualMedia.getViewMode();
if (viewMode != RavenMediaViewMode.PERMANENT) {
final MediaItemType mediaType = media.getType();
final boolean expired = TextUtils.isEmpty(media.getId());
final int info;
switch (mediaType) {
case MEDIA_TYPE_IMAGE:
if (expired) {
info = R.string.raven_image_expired;
break;
}
info = R.string.raven_image_info;
break;
case MEDIA_TYPE_VIDEO:
if (expired) {
info = R.string.raven_video_expired;
break;
}
info = R.string.raven_video_info;
break;
default:
if (expired) {
info = R.string.raven_msg_expired;
break;
}
info = R.string.raven_msg_info;
break;
}
binding.expiryInfo.setVisibility(View.VISIBLE);
binding.expiryInfo.setText(info);
return;
}
binding.expiryInfo.setVisibility(View.GONE);
}
private void setPreview(final DirectItemVisualMedia visualMedia,
final MessageDirection messageDirection) {
final Media media = visualMedia.getMedia();
final boolean expired = TextUtils.isEmpty(media.getId());
if (expired) {
binding.preview.setVisibility(View.GONE);
binding.typeIcon.setVisibility(View.GONE);
return;
}
final RoundingParams roundingParams = messageDirection == MessageDirection.INCOMING
? RoundingParams.fromCornersRadii(dmRadiusSmall, dmRadius, dmRadius, dmRadius)
: RoundingParams.fromCornersRadii(dmRadius, dmRadiusSmall, dmRadius, dmRadius);
binding.preview.setHierarchy(new GenericDraweeHierarchyBuilder(itemView.getResources())
.setRoundingParams(roundingParams)
.setActualImageScaleType(ScalingUtils.ScaleType.CENTER_CROP)
.build());
final MediaItemType modelMediaType = media.getType();
binding.typeIcon.setVisibility(modelMediaType == MediaItemType.MEDIA_TYPE_VIDEO || modelMediaType == MediaItemType.MEDIA_TYPE_SLIDER
? View.VISIBLE
: View.GONE);
final NullSafePair widthHeight = NumberUtils.calculateWidthHeight(
media.getOriginalHeight(),
media.getOriginalWidth(),
mediaImageMaxHeight,
maxWidth
);
final ViewGroup.LayoutParams layoutParams = binding.preview.getLayoutParams();
layoutParams.width = widthHeight.first;
layoutParams.height = widthHeight.second;
binding.preview.requestLayout();
final String thumbUrl = ResponseBodyUtils.getThumbUrl(media);
binding.preview.setImageURI(thumbUrl);
}
@Override
protected boolean allowLongClick() {
return false; // disabling until confirmed
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemReelShareViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder.directmessages;
import android.view.Gravity;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
import com.facebook.drawee.generic.RoundingParams;
import com.google.common.collect.ImmutableList;
import java.util.List;
import awais.instagrabber.R;
import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemCallback;
import awais.instagrabber.customviews.DirectItemContextMenu;
import awais.instagrabber.databinding.LayoutDmBaseBinding;
import awais.instagrabber.databinding.LayoutDmReelShareBinding;
import awais.instagrabber.models.enums.MediaItemType;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectItemReelShare;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
import awais.instagrabber.utils.ResponseBodyUtils;
import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils;
public class DirectItemReelShareViewHolder extends DirectItemViewHolder {
private final LayoutDmReelShareBinding binding;
private String type;
public DirectItemReelShareViewHolder(@NonNull final LayoutDmBaseBinding baseBinding,
@NonNull final LayoutDmReelShareBinding binding,
final User currentUser,
final DirectThread thread,
final DirectItemCallback callback) {
super(baseBinding, currentUser, thread, callback);
this.binding = binding;
setItemView(binding.getRoot());
}
@Override
public void bindItem(final DirectItem item, final MessageDirection messageDirection) {
final DirectItemReelShare reelShare = item.getReelShare();
type = reelShare.getType();
if (type == null) return;
final boolean isSelf = isSelf(item);
final Media media = reelShare.getMedia();
if (media == null) return;
final User user = media.getUser();
if (user == null) return;
final boolean expired = media.getType() == null;
if (expired) {
binding.preview.setVisibility(View.GONE);
binding.typeIcon.setVisibility(View.GONE);
binding.quoteLine.setVisibility(View.GONE);
binding.reaction.setVisibility(View.GONE);
} else {
binding.preview.setVisibility(View.VISIBLE);
binding.typeIcon.setVisibility(View.VISIBLE);
binding.quoteLine.setVisibility(View.VISIBLE);
binding.reaction.setVisibility(View.VISIBLE);
}
setGravity(messageDirection, expired);
if (type.equals("reply")) {
setReply(messageDirection, reelShare, isSelf);
}
if (type.equals("reaction")) {
setReaction(messageDirection, reelShare, isSelf, expired);
}
if (type.equals("mention")) {
setMention(isSelf);
}
if (!expired) {
setPreview(media);
itemView.setOnClickListener(v -> openMedia(media, -1));
}
}
private void setGravity(final MessageDirection messageDirection, final boolean expired) {
final boolean isIncoming = messageDirection == MessageDirection.INCOMING;
binding.shareInfo.setGravity(isIncoming ? Gravity.START : Gravity.END);
if (!expired) {
binding.quoteLine.setVisibility(isIncoming ? View.VISIBLE : View.GONE);
binding.quoteLineEnd.setVisibility(isIncoming ? View.GONE : View.VISIBLE);
}
final ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) binding.quoteLine.getLayoutParams();
layoutParams.horizontalBias = isIncoming ? 0 : 1;
final ConstraintLayout.LayoutParams messageLayoutParams = (ConstraintLayout.LayoutParams) binding.message.getLayoutParams();
messageLayoutParams.startToStart = isIncoming ? ConstraintLayout.LayoutParams.PARENT_ID : ConstraintLayout.LayoutParams.UNSET;
messageLayoutParams.endToEnd = isIncoming ? ConstraintLayout.LayoutParams.UNSET : ConstraintLayout.LayoutParams.PARENT_ID;
messageLayoutParams.setMarginStart(isIncoming ? messageInfoPaddingSmall : 0);
messageLayoutParams.setMarginEnd(isIncoming ? 0 : messageInfoPaddingSmall);
final ConstraintLayout.LayoutParams reactionLayoutParams = (ConstraintLayout.LayoutParams) binding.reaction.getLayoutParams();
final int previewId = binding.preview.getId();
if (isIncoming) {
reactionLayoutParams.startToEnd = previewId;
reactionLayoutParams.endToEnd = previewId;
reactionLayoutParams.startToStart = ConstraintLayout.LayoutParams.UNSET;
reactionLayoutParams.endToStart = ConstraintLayout.LayoutParams.UNSET;
} else {
reactionLayoutParams.startToStart = previewId;
reactionLayoutParams.endToStart = previewId;
reactionLayoutParams.startToEnd = ConstraintLayout.LayoutParams.UNSET;
reactionLayoutParams.endToEnd = ConstraintLayout.LayoutParams.UNSET;
}
}
private void setReply(final MessageDirection messageDirection,
final DirectItemReelShare reelShare,
final boolean isSelf) {
final int info = isSelf ? R.string.replied_story_outgoing : R.string.replied_story_incoming;
binding.shareInfo.setText(info);
binding.reaction.setVisibility(View.GONE);
final String text = reelShare.getText();
if (TextUtils.isEmpty(text)) {
binding.message.setVisibility(View.GONE);
return;
}
setMessage(messageDirection, text);
}
private void setReaction(final MessageDirection messageDirection,
final DirectItemReelShare reelShare,
final boolean isSelf,
final boolean expired) {
final int info = isSelf ? R.string.reacted_story_outgoing : R.string.reacted_story_incoming;
binding.shareInfo.setText(info);
binding.message.setVisibility(View.GONE);
final String text = reelShare.getText();
if (TextUtils.isEmpty(text)) {
binding.reaction.setVisibility(View.GONE);
return;
}
if (expired) {
setMessage(messageDirection, text);
return;
}
binding.reaction.setVisibility(View.VISIBLE);
binding.reaction.setText(text);
}
private void setMention(final boolean isSelf) {
final int info = isSelf ? R.string.mentioned_story_outgoing : R.string.mentioned_story_incoming;
binding.shareInfo.setText(info);
binding.message.setVisibility(View.GONE);
binding.reaction.setVisibility(View.GONE);
}
private void setMessage(final MessageDirection messageDirection, final String text) {
binding.message.setVisibility(View.VISIBLE);
binding.message.setBackgroundResource(messageDirection == MessageDirection.INCOMING
? R.drawable.bg_speech_bubble_incoming
: R.drawable.bg_speech_bubble_outgoing);
binding.message.setText(text);
}
private void setPreview(final Media media) {
final MediaItemType mediaType = media.getType();
if (mediaType == null) return;
binding.typeIcon.setVisibility(mediaType == MediaItemType.MEDIA_TYPE_VIDEO || mediaType == MediaItemType.MEDIA_TYPE_SLIDER
? View.VISIBLE : View.GONE);
final RoundingParams roundingParams = RoundingParams.fromCornersRadii(dmRadiusSmall, dmRadiusSmall, dmRadiusSmall, dmRadiusSmall);
binding.preview.setHierarchy(new GenericDraweeHierarchyBuilder(itemView.getResources())
.setRoundingParams(roundingParams)
.build());
final String thumbUrl = ResponseBodyUtils.getThumbUrl(media);
binding.preview.setImageURI(thumbUrl);
}
@Override
protected boolean canForward() {
return false;
}
@Override
protected List getLongClickOptions() {
final ImmutableList.Builder builder = ImmutableList.builder();
if (type != null && type.equals("reply")) {
builder.add(new DirectItemContextMenu.MenuItem(R.id.copy, R.string.copy_reply, item -> {
final DirectItemReelShare reelShare = item.getReelShare();
if (reelShare == null) return null;
final String text = reelShare.getText();
if (TextUtils.isEmpty(text)) return null;
Utils.copyText(itemView.getContext(), text);
return null;
}));
}
return builder.build();
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemStoryShareViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder.directmessages;
import android.content.res.Resources;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.ItemTouchHelper;
import com.facebook.drawee.drawable.ScalingUtils;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
import com.facebook.drawee.generic.RoundingParams;
import awais.instagrabber.R;
import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemCallback;
import awais.instagrabber.databinding.LayoutDmBaseBinding;
import awais.instagrabber.databinding.LayoutDmStoryShareBinding;
import awais.instagrabber.models.enums.MediaItemType;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectItemStoryShare;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
import awais.instagrabber.utils.NullSafePair;
import awais.instagrabber.utils.NumberUtils;
import awais.instagrabber.utils.ResponseBodyUtils;
import awais.instagrabber.utils.TextUtils;
public class DirectItemStoryShareViewHolder extends DirectItemViewHolder {
private final LayoutDmStoryShareBinding binding;
public DirectItemStoryShareViewHolder(@NonNull final LayoutDmBaseBinding baseBinding,
@NonNull final LayoutDmStoryShareBinding binding,
final User currentUser,
final DirectThread thread,
final DirectItemCallback callback) {
super(baseBinding, currentUser, thread, callback);
this.binding = binding;
setItemView(binding.getRoot());
}
@Override
public void bindItem(final DirectItem item, final MessageDirection messageDirection) {
final Resources resources = itemView.getResources();
int format = R.string.story_share;
final String reelType = item.getStoryShare().getReelType();
if (reelType == null || item.getStoryShare().getMedia() == null) {
setExpiredStoryInfo(item);
return;
}
if (reelType.equals("highlight_reel")) {
format = R.string.story_share_highlight;
}
final User user = item.getStoryShare().getMedia().getUser();
final String info = resources.getString(format, user != null ? user.getUsername() : "");
binding.shareInfo.setText(info);
binding.text.setVisibility(View.GONE);
binding.ivMediaPreview.setController(null);
final DirectItemStoryShare storyShare = item.getStoryShare();
if (storyShare == null) return;
setText(storyShare);
final Media media = storyShare.getMedia();
setupPreview(messageDirection, media);
itemView.setOnClickListener(v -> openStory(storyShare));
}
private void setupPreview(final MessageDirection messageDirection, final Media storyShareMedia) {
final MediaItemType mediaType = storyShareMedia.getType();
binding.typeIcon.setVisibility(mediaType == MediaItemType.MEDIA_TYPE_VIDEO ? View.VISIBLE : View.GONE);
final RoundingParams roundingParams = messageDirection == MessageDirection.INCOMING
? RoundingParams.fromCornersRadii(dmRadiusSmall, dmRadius, dmRadius, dmRadius)
: RoundingParams.fromCornersRadii(dmRadius, dmRadiusSmall, dmRadius, dmRadius);
binding.ivMediaPreview.setHierarchy(new GenericDraweeHierarchyBuilder(itemView.getResources())
.setRoundingParams(roundingParams)
.setActualImageScaleType(ScalingUtils.ScaleType.CENTER_CROP)
.build());
final NullSafePair widthHeight = NumberUtils.calculateWidthHeight(
storyShareMedia.getOriginalHeight(),
storyShareMedia.getOriginalWidth(),
mediaImageMaxHeight,
mediaImageMaxWidth
);
final ViewGroup.LayoutParams layoutParams = binding.ivMediaPreview.getLayoutParams();
layoutParams.width = widthHeight.first;
layoutParams.height = widthHeight.second;
binding.ivMediaPreview.requestLayout();
final String thumbUrl = ResponseBodyUtils.getThumbUrl(storyShareMedia);
binding.ivMediaPreview.setImageURI(thumbUrl);
}
private void setText(final DirectItemStoryShare storyShare) {
final String text = storyShare.getText();
if (!TextUtils.isEmpty(text)) {
binding.text.setText(text);
binding.text.setVisibility(View.VISIBLE);
return;
}
binding.text.setVisibility(View.GONE);
}
private void setExpiredStoryInfo(final DirectItem item) {
binding.shareInfo.setText(item.getStoryShare().getTitle());
binding.text.setVisibility(View.VISIBLE);
binding.text.setText(item.getStoryShare().getMessage());
binding.ivMediaPreview.setVisibility(View.GONE);
binding.typeIcon.setVisibility(View.GONE);
}
@Override
protected boolean canForward() {
return false;
}
@Override
public int getSwipeDirection() {
return ItemTouchHelper.ACTION_STATE_IDLE;
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemTextViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder.directmessages;
import androidx.annotation.NonNull;
import com.google.common.collect.ImmutableList;
import java.util.List;
import awais.instagrabber.R;
import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemCallback;
import awais.instagrabber.customviews.DirectItemContextMenu;
import awais.instagrabber.databinding.LayoutDmBaseBinding;
import awais.instagrabber.databinding.LayoutDmTextBinding;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils;
public class DirectItemTextViewHolder extends DirectItemViewHolder {
private final LayoutDmTextBinding binding;
public DirectItemTextViewHolder(@NonNull final LayoutDmBaseBinding baseBinding,
@NonNull final LayoutDmTextBinding binding,
final User currentUser,
final DirectThread thread,
@NonNull final DirectItemCallback callback) {
super(baseBinding, currentUser, thread, callback);
this.binding = binding;
setItemView(binding.getRoot());
}
@Override
public void bindItem(final DirectItem directItemModel, final MessageDirection messageDirection) {
final String text = directItemModel.getText();
if (text == null) return;
binding.tvMessage.setText(text);
setupRamboTextListeners(binding.tvMessage);
}
@Override
protected boolean showBackground() {
return true;
}
@Override
protected List getLongClickOptions() {
return ImmutableList.of(
new DirectItemContextMenu.MenuItem(R.id.copy, R.string.copy, item -> {
if (TextUtils.isEmpty(item.getText())) return null;
Utils.copyText(itemView.getContext(), item.getText());
return null;
})
);
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemVideoCallEventViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder.directmessages;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.style.ClickableSpan;
import android.text.style.ForegroundColorSpan;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.ItemTouchHelper;
import java.util.List;
import awais.instagrabber.R;
import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemCallback;
import awais.instagrabber.databinding.LayoutDmActionLogBinding;
import awais.instagrabber.databinding.LayoutDmBaseBinding;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectItemVideoCallEvent;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
import awais.instagrabber.repositories.responses.directmessages.TextRange;
import awais.instagrabber.utils.TextUtils;
public class DirectItemVideoCallEventViewHolder extends DirectItemViewHolder {
private final LayoutDmActionLogBinding binding;
public DirectItemVideoCallEventViewHolder(@NonNull final LayoutDmBaseBinding baseBinding,
final LayoutDmActionLogBinding binding,
final User currentUser,
final DirectThread thread,
final DirectItemCallback callback) {
super(baseBinding, currentUser, thread, callback);
this.binding = binding;
setItemView(binding.getRoot());
}
@Override
public void bindItem(final DirectItem directItemModel, final MessageDirection messageDirection) {
final DirectItemVideoCallEvent videoCallEvent = directItemModel.getVideoCallEvent();
final String text = videoCallEvent.getDescription();
final SpannableStringBuilder sb = new SpannableStringBuilder(text);
final List textAttributes = videoCallEvent.getTextAttributes();
if (textAttributes != null && !textAttributes.isEmpty()) {
for (final TextRange textAttribute : textAttributes) {
if (!TextUtils.isEmpty(textAttribute.getColor())) {
final ForegroundColorSpan colorSpan = new ForegroundColorSpan(itemView.getResources().getColor(R.color.deep_orange_400));
sb.setSpan(colorSpan, textAttribute.getStart(), textAttribute.getEnd(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
}
if (!TextUtils.isEmpty(textAttribute.getIntent())) {
final ClickableSpan clickableSpan = new ClickableSpan() {
@Override
public void onClick(@NonNull final View widget) {
}
};
sb.setSpan(clickableSpan, textAttribute.getStart(), textAttribute.getEnd(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
}
}
}
binding.tvMessage.setMaxLines(1);
binding.tvMessage.setText(sb);
}
@Override
protected boolean allowMessageDirectionGravity() {
return false;
}
@Override
protected boolean showUserDetailsInGroup() {
return false;
}
@Override
protected boolean showMessageInfo() {
return false;
}
@Override
protected boolean allowLongClick() {
return false;
}
@Override
public int getSwipeDirection() {
return ItemTouchHelper.ACTION_STATE_IDLE;
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder.directmessages;
import android.annotation.SuppressLint;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.drawable.Drawable;
import android.view.Gravity;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewPropertyAnimator;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.FrameLayout;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.widget.ImageViewCompat;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import androidx.transition.TransitionManager;
import com.google.android.material.transition.MaterialFade;
import com.google.common.collect.ImmutableList;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
import awais.instagrabber.R;
import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemCallback;
import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemInternalLongClickListener;
import awais.instagrabber.customviews.DirectItemContextMenu;
import awais.instagrabber.customviews.DirectItemFrameLayout;
import awais.instagrabber.customviews.RamboTextViewV2;
import awais.instagrabber.customviews.helpers.SwipeAndRestoreItemTouchHelperCallback.SwipeableViewHolder;
import awais.instagrabber.databinding.LayoutDmBaseBinding;
import awais.instagrabber.models.enums.DirectItemType;
import awais.instagrabber.models.enums.MediaItemType;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectItemEmojiReaction;
import awais.instagrabber.repositories.responses.directmessages.DirectItemReactions;
import awais.instagrabber.repositories.responses.directmessages.DirectItemStoryShare;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
import awais.instagrabber.utils.DMUtils;
import awais.instagrabber.utils.DeepLinkParser;
import awais.instagrabber.utils.ResponseBodyUtils;
public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder implements SwipeableViewHolder {
private static final String TAG = DirectItemViewHolder.class.getSimpleName();
// private static final List THREAD_CHANGING_OPTIONS = ImmutableList.of(R.id.unsend);
private final LayoutDmBaseBinding binding;
private final User currentUser;
private final DirectThread thread;
private final int groupMessageWidth;
private final List userIds;
private final DirectItemCallback callback;
private final int reactionAdjustMargin;
private final AccelerateDecelerateInterpolator accelerateDecelerateInterpolator = new AccelerateDecelerateInterpolator();
protected final int margin;
protected final int dmRadius;
protected final int dmRadiusSmall;
protected final int messageInfoPaddingSmall;
protected final int mediaImageMaxHeight;
protected final int windowWidth;
protected final int mediaImageMaxWidth;
protected final int reactionTranslationYType1;
protected final int reactionTranslationYType2;
private boolean selected = false;
private DirectItemInternalLongClickListener longClickListener;
private DirectItem item;
private ViewPropertyAnimator shrinkGrowAnimator;
private MessageDirection messageDirection;
// private View.OnLayoutChangeListener layoutChangeListener;
public DirectItemViewHolder(@NonNull final LayoutDmBaseBinding binding,
@NonNull final User currentUser,
@NonNull final DirectThread thread,
@NonNull final DirectItemCallback callback) {
super(binding.getRoot());
this.binding = binding;
this.currentUser = currentUser;
this.thread = thread;
this.callback = callback;
userIds = thread.getUsers()
.stream()
.map(User::getPk)
.collect(Collectors.toList());
binding.ivProfilePic.setVisibility(thread.isGroup() ? View.VISIBLE : View.GONE);
binding.ivProfilePic.setOnClickListener(null);
final Resources resources = itemView.getResources();
margin = resources.getDimensionPixelSize(R.dimen.dm_message_item_margin);
final int avatarSize = resources.getDimensionPixelSize(R.dimen.dm_message_item_avatar_size);
dmRadius = resources.getDimensionPixelSize(R.dimen.dm_message_card_radius);
dmRadiusSmall = resources.getDimensionPixelSize(R.dimen.dm_message_card_radius_small);
messageInfoPaddingSmall = resources.getDimensionPixelSize(R.dimen.dm_message_info_padding_small);
windowWidth = resources.getDisplayMetrics().widthPixels;
mediaImageMaxHeight = resources.getDimensionPixelSize(R.dimen.dm_media_img_max_height);
reactionAdjustMargin = resources.getDimensionPixelSize(R.dimen.dm_reaction_adjust_margin);
final int groupWidthCorrection = avatarSize + messageInfoPaddingSmall * 3;
mediaImageMaxWidth = windowWidth - margin - (thread.isGroup() ? groupWidthCorrection : messageInfoPaddingSmall * 2);
// messageInfoPaddingSmall is used cuz it's also 4dp, 1 avatar margin + 2 paddings = 3
groupMessageWidth = windowWidth - margin - groupWidthCorrection;
reactionTranslationYType1 = resources.getDimensionPixelSize(R.dimen.dm_reaction_translation_y_type_1);
reactionTranslationYType2 = resources.getDimensionPixelSize(R.dimen.dm_reaction_translation_y_type_2);
}
public void bind(final int position, final DirectItem item) {
if (item == null) return;
this.item = item;
messageDirection = isSelf(item) ? MessageDirection.OUTGOING : MessageDirection.INCOMING;
// Asynchronous binding causes some weird behaviour
// itemView.post(() -> bindBase(item, messageDirection, position));
// itemView.post(() -> bindItem(item, messageDirection));
// itemView.post(() -> setupLongClickListener(position, messageDirection));
bindBase(item, messageDirection, position);
bindItem(item, messageDirection);
setupLongClickListener(position, messageDirection);
}
private void bindBase(@NonNull final DirectItem item, final MessageDirection messageDirection, final int position) {
final FrameLayout.LayoutParams containerLayoutParams = (FrameLayout.LayoutParams) binding.container.getLayoutParams();
final DirectItemType itemType = item.getItemType() == null ? DirectItemType.UNKNOWN : item.getItemType();
setMessageDirectionGravity(messageDirection, containerLayoutParams);
setGroupUserDetails(item, messageDirection);
setBackground(messageDirection);
setMessageInfo(item, messageDirection);
switch (itemType) {
case REEL_SHARE:
case STORY_SHARE: // i think they could have texts?
// containerLayoutParams.setMarginStart(0);
// containerLayoutParams.setMarginEnd(0);
case TEXT:
case LINK:
case UNKNOWN:
binding.messageInfo.setPadding(0, 0, dmRadius, dmRadiusSmall);
break;
default:
if (showMessageInfo()) {
binding.messageInfo.setPadding(0, 0, messageInfoPaddingSmall, dmRadiusSmall);
}
}
setupReply(item, messageDirection);
setReactions(item, position);
if (item.getRepliedToMessage() == null && item.getShowForwardAttribution()) {
setForwardInfo(messageDirection);
}
}
private void setBackground(final MessageDirection messageDirection) {
if (showBackground()) {
binding.background.setBackgroundResource(messageDirection == MessageDirection.INCOMING ? R.drawable.bg_speech_bubble_incoming
: R.drawable.bg_speech_bubble_outgoing);
return;
}
binding.background.setBackgroundResource(0);
}
private void setGroupUserDetails(final DirectItem item, final MessageDirection messageDirection) {
if (showUserDetailsInGroup()) {
binding.ivProfilePic.setVisibility(messageDirection == MessageDirection.INCOMING && thread.isGroup() ? View.VISIBLE : View.GONE);
binding.tvUsername.setVisibility(messageDirection == MessageDirection.INCOMING && thread.isGroup() ? View.VISIBLE : View.GONE);
if (messageDirection == MessageDirection.INCOMING && thread.isGroup()) {
final List allUsers = new LinkedList(thread.getUsers());
allUsers.addAll(thread.getLeftUsers());
final User user = getUser(item.getUserId(), allUsers);
if (user != null) {
binding.tvUsername.setText(user.getUsername());
binding.ivProfilePic.setImageURI(user.getProfilePicUrl());
}
ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) binding.chatMessageLayout.getLayoutParams();
layoutParams.matchConstraintMaxWidth = groupMessageWidth;
binding.chatMessageLayout.setLayoutParams(layoutParams);
}
return;
}
binding.ivProfilePic.setVisibility(View.GONE);
binding.tvUsername.setVisibility(View.GONE);
}
private void setMessageDirectionGravity(final MessageDirection messageDirection,
final FrameLayout.LayoutParams containerLayoutParams) {
if (allowMessageDirectionGravity()) {
containerLayoutParams.setMarginStart(messageDirection == MessageDirection.OUTGOING ? margin : 0);
containerLayoutParams.setMarginEnd(messageDirection == MessageDirection.INCOMING ? margin : 0);
containerLayoutParams.gravity = messageDirection == MessageDirection.INCOMING ? Gravity.START : Gravity.END;
return;
}
containerLayoutParams.gravity = Gravity.CENTER;
}
private void setMessageInfo(@NonNull final DirectItem item, final MessageDirection messageDirection) {
if (showMessageInfo()) {
binding.messageInfo.setVisibility(View.VISIBLE);
binding.deliveryStatus.setVisibility(messageDirection == MessageDirection.OUTGOING ? View.VISIBLE : View.GONE);
if (item.getDate() != null) {
final DateTimeFormatter dateFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT);
binding.messageTime.setText(dateFormatter.format(item.getDate()));
}
if (messageDirection == MessageDirection.OUTGOING) {
if (item.isPending()) {
binding.deliveryStatus.setImageResource(R.drawable.ic_check_24);
} else {
final boolean read = DMUtils.isRead(item,
thread.getLastSeenAt(),
userIds
);
binding.deliveryStatus.setImageResource(R.drawable.ic_check_all_24);
ImageViewCompat.setImageTintList(
binding.deliveryStatus,
ColorStateList.valueOf(itemView.getResources().getColor(read ? R.color.blue_500 : R.color.grey_500))
);
}
}
return;
}
binding.messageInfo.setVisibility(View.GONE);
}
private void setupReply(final DirectItem item, final MessageDirection messageDirection) {
if (item.getRepliedToMessage() != null) {
final List allUsers = new LinkedList(thread.getUsers());
allUsers.addAll(thread.getLeftUsers());
setReply(item, messageDirection, allUsers);
} else {
binding.quoteLine.setVisibility(View.GONE);
binding.replyContainer.setVisibility(View.GONE);
binding.replyInfo.setVisibility(View.GONE);
}
}
private void setReply(final DirectItem item,
final MessageDirection messageDirection,
final List users) {
final DirectItem replied = item.getRepliedToMessage();
final DirectItemType itemType = replied.getItemType();
final Resources resources = itemView.getResources();
String text = null;
String url = null;
switch (itemType) {
case TEXT:
text = replied.getText();
break;
case LINK:
text = replied.getLink().getText();
break;
case PLACEHOLDER:
text = replied.getPlaceholder().getMessage();
break;
case MEDIA:
url = ResponseBodyUtils.getThumbUrl(replied.getMedia());
break;
case RAVEN_MEDIA:
url = ResponseBodyUtils.getThumbUrl(replied.getVisualMedia().getMedia());
break;
case VOICE_MEDIA:
text = resources.getString(R.string.voice_message);
break;
case MEDIA_SHARE:
Media mediaShare = replied.getMediaShare();
if (mediaShare.getType() == MediaItemType.MEDIA_TYPE_SLIDER) {
mediaShare = mediaShare.getCarouselMedia().get(0);
}
url = ResponseBodyUtils.getThumbUrl(mediaShare);
break;
case REEL_SHARE:
text = replied.getReelShare().getText();
break;
// Below types cannot be replied to
// case LIKE:
// text = "❤️";
// break;
// case PROFILE:
// text = "@" + replied.getProfile().getUsername();
// break;
// case CLIP:
// url = ResponseBodyUtils.getThumbUrl(replied.getClip().getClip().getImageVersions2());
// break;
// case FELIX_SHARE:
// url = ResponseBodyUtils.getThumbUrl(replied.getFelixShare().getVideo().getImageVersions2());
// break;
// case STORY_SHARE:
// final DirectItemMedia media = replied.getStoryShare().getMedia();
// if (media == null) break;
// url = ResponseBodyUtils.getThumbUrl(media.getImageVersions2());
// break;
// case LOCATION
}
if (text == null && url == null) {
binding.quoteLine.setVisibility(View.GONE);
binding.replyContainer.setVisibility(View.GONE);
binding.replyInfo.setVisibility(View.GONE);
return;
}
setReplyGravity(messageDirection);
final String info = setReplyInfo(item, replied, users, resources);
binding.replyInfo.setVisibility(View.VISIBLE);
binding.replyInfo.setText(info);
binding.quoteLine.setVisibility(View.VISIBLE);
binding.replyContainer.setVisibility(View.VISIBLE);
if (url != null) {
binding.replyText.setVisibility(View.GONE);
binding.replyImage.setVisibility(View.VISIBLE);
binding.replyImage.setImageURI(url);
return;
}
binding.replyImage.setVisibility(View.GONE);
final Drawable background = binding.replyText.getBackground().mutate();
background.setTint(replied.getUserId() != currentUser.getPk()
? resources.getColor(R.color.grey_600)
: resources.getColor(R.color.deep_purple_400));
binding.replyText.setBackgroundDrawable(background);
binding.replyText.setVisibility(View.VISIBLE);
binding.replyText.setText(text);
}
private String setReplyInfo(final DirectItem item,
final DirectItem replied,
final List users,
final Resources resources) {
final long repliedToUserId = replied.getUserId();
if (repliedToUserId == item.getUserId() && item.getUserId() == currentUser.getPk()) {
// User replied to own message
return resources.getString(R.string.replied_to_yourself);
}
if (repliedToUserId == item.getUserId()) {
// opposite user replied to their own message
return resources.getString(R.string.replied_to_themself);
}
final User user = getUser(repliedToUserId, users);
final String repliedToUsername = user != null ? user.getUsername() : "";
if (item.getUserId() == currentUser.getPk()) {
return thread.isGroup()
? resources.getString(R.string.replied_you_group, repliedToUsername)
: resources.getString(R.string.replied_you);
}
if (repliedToUserId == currentUser.getPk()) {
return resources.getString(R.string.replied_to_you);
}
return resources.getString(R.string.replied_group, repliedToUsername);
}
private void setForwardInfo(final MessageDirection direction) {
binding.replyInfo.setVisibility(View.VISIBLE);
binding.replyInfo.setText(direction == MessageDirection.OUTGOING ? R.string.forward_outgoing : R.string.forward_incoming);
}
private void setReplyGravity(final MessageDirection messageDirection) {
final boolean isIncoming = messageDirection == MessageDirection.INCOMING;
final ConstraintLayout.LayoutParams quoteLineLayoutParams = (ConstraintLayout.LayoutParams) binding.quoteLine.getLayoutParams();
final ConstraintLayout.LayoutParams replyContainerLayoutParams = (ConstraintLayout.LayoutParams) binding.replyContainer.getLayoutParams();
final ConstraintLayout.LayoutParams replyInfoLayoutParams = (ConstraintLayout.LayoutParams) binding.replyInfo.getLayoutParams();
final int profilePicId = binding.ivProfilePic.getId();
final int replyContainerId = binding.replyContainer.getId();
final int quoteLineId = binding.quoteLine.getId();
quoteLineLayoutParams.startToEnd = isIncoming ? profilePicId : replyContainerId;
quoteLineLayoutParams.endToStart = isIncoming ? replyContainerId : ConstraintLayout.LayoutParams.UNSET;
quoteLineLayoutParams.endToEnd = isIncoming ? ConstraintLayout.LayoutParams.UNSET : ConstraintLayout.LayoutParams.PARENT_ID;
replyContainerLayoutParams.startToEnd = isIncoming ? quoteLineId : profilePicId;
replyContainerLayoutParams.endToEnd = isIncoming ? ConstraintLayout.LayoutParams.PARENT_ID : ConstraintLayout.LayoutParams.UNSET;
replyContainerLayoutParams.endToStart = isIncoming ? ConstraintLayout.LayoutParams.UNSET : quoteLineId;
replyInfoLayoutParams.startToEnd = isIncoming ? quoteLineId : ConstraintLayout.LayoutParams.UNSET;
replyInfoLayoutParams.endToStart = isIncoming ? ConstraintLayout.LayoutParams.UNSET : quoteLineId;
}
private void setReactions(final DirectItem item, final int position) {
binding.getRoot().post(() -> {
MaterialFade materialFade = new MaterialFade();
materialFade.addTarget(binding.emojis);
TransitionManager.beginDelayedTransition(binding.getRoot(), materialFade);
final DirectItemReactions reactions = item.getReactions();
final List emojis = reactions != null ? reactions.getEmojis() : null;
if (emojis == null || emojis.isEmpty()) {
binding.container.setPadding(messageInfoPaddingSmall, messageInfoPaddingSmall, messageInfoPaddingSmall, 0);
binding.reactionsWrapper.setVisibility(View.GONE);
return;
}
binding.reactionsWrapper.setVisibility(View.VISIBLE);
binding.reactionsWrapper.setTranslationY(getReactionsTranslationY());
binding.container.setPadding(messageInfoPaddingSmall, messageInfoPaddingSmall, messageInfoPaddingSmall, reactionAdjustMargin);
binding.emojis.setEmojis(emojis.stream()
.map(DirectItemEmojiReaction::getEmoji)
.collect(Collectors.toList()));
// binding.emojis.setEmojis(ImmutableList.of("😣",
// "😖",
// "😫",
// "😩",
// "🥺",
// "😢",
// "😭",
// "😤",
// "😠",
// "😡",
// "🤬"));
binding.emojis.setOnClickListener(v -> callback.onReactionClick(item, position));
// final List reactedUsers = emojis.stream()
// .map(DirectItemEmojiReaction::getSenderId)
// .distinct()
// .map(userId -> getUser(userId, users))
// .collect(Collectors.toList());
// for (final DirectUser user : reactedUsers) {
// if (user == null) continue;
// final ProfilePicView profilePicView = new ProfilePicView(itemView.getContext());
// profilePicView.setSize(ProfilePicView.Size.TINY);
// profilePicView.setImageURI(user.getProfilePicUrl());
// binding.reactions.addView(profilePicView);
// }
});
}
protected boolean isSelf(final DirectItem directItem) {
return directItem.getUserId() == currentUser.getPk();
}
public void setItemView(final View view) {
this.binding.message.addView(view);
}
public abstract void bindItem(final DirectItem directItemModel, final MessageDirection messageDirection);
@Nullable
protected User getUser(final long userId, final List users) {
if (userId == currentUser.getPk()) {
return currentUser;
}
if (users == null) return null;
for (final User user : users) {
if (userId != user.getPk()) continue;
return user;
}
return null;
}
protected boolean allowMessageDirectionGravity() {
return true;
}
protected boolean showUserDetailsInGroup() {
return true;
}
protected boolean showBackground() {
return false;
}
protected boolean showMessageInfo() {
return true;
}
protected boolean allowLongClick() {
return true;
}
protected boolean allowReaction() {
return true;
}
protected boolean canForward() {
return true;
}
protected List getLongClickOptions() {
return null;
}
protected int getReactionsTranslationY() {
return reactionTranslationYType1;
}
@CallSuper
public void cleanup() {
// if (layoutChangeListener != null) {
// binding.container.removeOnLayoutChangeListener(layoutChangeListener);
// }
}
protected void setupRamboTextListeners(@NonNull final RamboTextViewV2 textView) {
textView.addOnHashtagListener(autoLinkItem -> callback.onHashtagClick(autoLinkItem.getOriginalText().trim()));
textView.addOnMentionClickListener(autoLinkItem -> openProfile(autoLinkItem.getOriginalText().trim()));
textView.addOnEmailClickListener(autoLinkItem -> callback.onEmailClick(autoLinkItem.getOriginalText().trim()));
textView.addOnURLClickListener(autoLinkItem -> openURL(autoLinkItem.getOriginalText().trim()));
}
protected void openProfile(final String username) {
callback.onMentionClick(username);
}
protected void openLocation(final long locationId) {
callback.onLocationClick(locationId);
}
protected void openURL(final String url) {
callback.onURLClick(url);
}
protected void openMedia(final Media media, final int index) {
callback.onMediaClick(media, index);
}
protected void openStory(final DirectItemStoryShare storyShare) {
callback.onStoryClick(storyShare);
}
protected void handleDeepLink(final String deepLinkText) {
if (deepLinkText == null) return;
final DeepLinkParser.DeepLink deepLink = DeepLinkParser.parse(deepLinkText);
if (deepLink == null) return;
switch (deepLink.getType()) {
case USER:
callback.onMentionClick(deepLink.getValue());
break;
}
}
@SuppressLint("ClickableViewAccessibility")
private void setupLongClickListener(final int position, final MessageDirection messageDirection) {
if (!allowLongClick()) return;
binding.getRoot().setOnItemLongClickListener(new DirectItemFrameLayout.OnItemLongClickListener() {
@Override
public void onLongClickStart(final View view) {
itemView.post(() -> shrink());
}
@Override
public void onLongClickCancel(final View view) {
itemView.post(() -> grow());
}
@Override
public void onLongClick(final View view, final float x, final float y) {
// if (longClickListener == null) return false;
// longClickListener.onLongClick(position, this);
itemView.post(() -> grow());
setSelected(true);
showLongClickOptions(new Point((int) x, (int) y), messageDirection);
}
});
}
private void showLongClickOptions(final Point location, final MessageDirection messageDirection) {
final List longClickOptions = getLongClickOptions();
final ImmutableList.Builder builder = ImmutableList.builder();
if (longClickOptions != null) {
builder.addAll(longClickOptions);
}
if (canForward()) {
builder.add(new DirectItemContextMenu.MenuItem(R.id.forward, R.string.forward));
}
if (thread.getInputMode() != 1 && messageDirection == MessageDirection.OUTGOING) {
builder.add(new DirectItemContextMenu.MenuItem(R.id.unsend, R.string.dms_inbox_unsend));
}
final boolean showReactions = thread.getInputMode() != 1 && allowReaction();
final ImmutableList menuItems = builder.build();
if (!showReactions && menuItems.isEmpty()) return;
final DirectItemContextMenu menu = new DirectItemContextMenu(itemView.getContext(), showReactions, menuItems);
menu.setOnDismissListener(() -> setSelected(false));
menu.setOnReactionClickListener(emoji -> callback.onReaction(item, emoji));
menu.setOnOptionSelectListener((itemId, cb) -> callback.onOptionSelect(item, itemId, cb));
menu.setOnAddReactionListener(() -> {
menu.dismiss();
itemView.postDelayed(() -> callback.onAddReactionListener(item), 300);
});
menu.show(itemView, location);
}
public void setLongClickListener(final DirectItemInternalLongClickListener longClickListener) {
this.longClickListener = longClickListener;
}
public void setSelected(final boolean selected) {
this.selected = selected;
}
private void shrink() {
if (shrinkGrowAnimator != null) {
shrinkGrowAnimator.cancel();
}
shrinkGrowAnimator = itemView.animate()
.scaleX(0.8f)
.scaleY(0.8f)
.setInterpolator(accelerateDecelerateInterpolator)
.setDuration(ViewConfiguration.getLongPressTimeout() - ViewConfiguration.getTapTimeout());
shrinkGrowAnimator.start();
}
private void grow() {
if (shrinkGrowAnimator != null) {
shrinkGrowAnimator.cancel();
}
shrinkGrowAnimator = itemView.animate()
.scaleX(1f)
.scaleY(1f)
.setInterpolator(accelerateDecelerateInterpolator)
.setDuration(200)
.withEndAction(() -> shrinkGrowAnimator = null);
shrinkGrowAnimator.start();
}
@Override
public int getSwipeDirection() {
if (item == null || messageDirection == null) return ItemTouchHelper.ACTION_STATE_IDLE;
return messageDirection == MessageDirection.OUTGOING ? ItemTouchHelper.START : ItemTouchHelper.END;
}
public enum MessageDirection {
INCOMING,
OUTGOING
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemVoiceMediaViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder.directmessages;
import android.os.Handler;
import android.util.Log;
import android.view.View;
import androidx.annotation.NonNull;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.common.collect.ImmutableList;
import com.google.common.primitives.Floats;
import java.util.List;
import awais.instagrabber.R;
import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemCallback;
import awais.instagrabber.customviews.DirectItemContextMenu;
import awais.instagrabber.databinding.LayoutDmBaseBinding;
import awais.instagrabber.databinding.LayoutDmVoiceMediaBinding;
import awais.instagrabber.repositories.responses.Audio;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectItemVoiceMedia;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
import awais.instagrabber.utils.TextUtils;
import static com.google.android.exoplayer2.C.TIME_UNSET;
public class DirectItemVoiceMediaViewHolder extends DirectItemViewHolder {
private static final String TAG = "DirectItemVoiceMediaVH";
private final LayoutDmVoiceMediaBinding binding;
private final DefaultDataSourceFactory dataSourceFactory;
private SimpleExoPlayer player;
private Handler handler;
private Runnable positionChecker;
private Player.EventListener listener;
public DirectItemVoiceMediaViewHolder(@NonNull final LayoutDmBaseBinding baseBinding,
@NonNull final LayoutDmVoiceMediaBinding binding,
final User currentUser,
final DirectThread thread,
final DirectItemCallback callback) {
super(baseBinding, currentUser, thread, callback);
this.binding = binding;
this.dataSourceFactory = new DefaultDataSourceFactory(binding.getRoot().getContext(), "instagram");
setItemView(binding.getRoot());
binding.voiceMedia.getLayoutParams().width = mediaImageMaxWidth;
}
@Override
public void bindItem(final DirectItem directItemModel, final MessageDirection messageDirection) {
final DirectItemVoiceMedia voiceMedia = directItemModel.getVoiceMedia();
if (voiceMedia == null) return;
final Media media = voiceMedia.getMedia();
if (media == null) return;
final Audio audio = media.getAudio();
if (audio == null) return;
final List waveformData = audio.getWaveformData();
binding.waveformSeekBar.setSample(Floats.toArray(waveformData));
binding.waveformSeekBar.setEnabled(false);
final String text = String.format("%s/%s", TextUtils.millisToTimeString(0), TextUtils.millisToTimeString(audio.getDuration()));
binding.duration.setText(text);
final AudioItemState audioItemState = new AudioItemState();
player = new SimpleExoPlayer.Builder(itemView.getContext()).build();
player.setVolume(1);
player.setPlayWhenReady(true);
player.setRepeatMode(Player.REPEAT_MODE_OFF);
handler = new Handler();
final long initialDelay = 0;
final long recurringDelay = 60;
positionChecker = new Runnable() {
@Override
public void run() {
if (handler != null) {
handler.removeCallbacks(this);
}
if (player == null) return;
final long currentPosition = player.getCurrentPosition();
final long duration = player.getDuration();
// Log.d(TAG, "currentPosition: " + currentPosition + ", duration: " + duration);
if (duration == TIME_UNSET) return;
// final float progress = ((float) currentPosition / duration /* * 100 */);
final int progress = (int) ((float) currentPosition / duration * 100);
// Log.d(TAG, "progress: " + progress);
final String text = String.format("%s/%s", TextUtils.millisToTimeString(currentPosition), TextUtils.millisToTimeString(duration));
binding.duration.setText(text);
binding.waveformSeekBar.setProgress(progress);
if (handler != null) {
handler.postDelayed(this, recurringDelay);
}
}
};
player.addListener(listener = new Player.EventListener() {
@Override
public void onPlaybackStateChanged(final int state) {
if (!audioItemState.isPrepared() && state == Player.STATE_READY) {
binding.playPause.setIconResource(R.drawable.ic_round_pause_24);
audioItemState.setPrepared(true);
binding.playPause.setVisibility(View.VISIBLE);
binding.progressBar.setVisibility(View.GONE);
if (handler != null) {
handler.postDelayed(positionChecker, initialDelay);
}
return;
}
if (state == Player.STATE_ENDED) {
// binding.waveformSeekBar.setProgressInPercentage(0);
binding.waveformSeekBar.setProgress(0);
binding.playPause.setIconResource(R.drawable.ic_round_play_arrow_24);
if (handler != null) {
handler.removeCallbacks(positionChecker);
}
}
}
@Override
public void onPlayerError(final ExoPlaybackException error) {
Log.e(TAG, "onPlayerError: ", error);
}
});
final ProgressiveMediaSource.Factory sourceFactory = new ProgressiveMediaSource.Factory(dataSourceFactory);
final MediaItem mediaItem = MediaItem.fromUri(audio.getAudioSrc());
final ProgressiveMediaSource mediaSource = sourceFactory.createMediaSource(mediaItem);
player.setMediaSource(mediaSource);
binding.playPause.setOnClickListener(v -> {
if (player == null) return;
if (!audioItemState.isPrepared()) {
player.prepare();
binding.playPause.setVisibility(View.GONE);
binding.progressBar.setVisibility(View.VISIBLE);
return;
}
if (player.isPlaying()) {
binding.playPause.setIconResource(R.drawable.ic_round_play_arrow_24);
player.pause();
return;
}
binding.playPause.setIconResource(R.drawable.ic_round_pause_24);
if (player.getPlaybackState() == Player.STATE_ENDED) {
player.seekTo(0);
if (handler != null) {
handler.postDelayed(positionChecker, initialDelay);
}
}
binding.waveformSeekBar.setEnabled(true);
player.play();
});
}
@Override
public void cleanup() {
super.cleanup();
if (handler != null && positionChecker != null) {
handler.removeCallbacks(positionChecker);
handler = null;
positionChecker = null;
}
if (player != null) {
player.release();
if (listener != null) {
player.removeListener(listener);
}
player = null;
}
}
@Override
protected boolean canForward() {
return false;
}
@Override
protected List getLongClickOptions() {
return ImmutableList.of(
new DirectItemContextMenu.MenuItem(R.id.download, R.string.action_download)
);
}
private static class AudioItemState {
private boolean prepared;
private AudioItemState() {}
public boolean isPrepared() {
return prepared;
}
public void setPrepared(final boolean prepared) {
this.prepared = prepared;
}
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemXmaViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder.directmessages;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.ItemTouchHelper;
import com.facebook.drawee.backends.pipeline.Fresco;
import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemCallback;
import awais.instagrabber.databinding.LayoutDmAnimatedMediaBinding;
import awais.instagrabber.databinding.LayoutDmBaseBinding;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectItemXma;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
import awais.instagrabber.repositories.responses.directmessages.XmaUrlInfo;
import awais.instagrabber.utils.NullSafePair;
import awais.instagrabber.utils.NumberUtils;
public class DirectItemXmaViewHolder extends DirectItemViewHolder {
private final LayoutDmAnimatedMediaBinding binding;
public DirectItemXmaViewHolder(@NonNull final LayoutDmBaseBinding baseBinding,
@NonNull final LayoutDmAnimatedMediaBinding binding,
final User currentUser,
final DirectThread thread,
final DirectItemCallback callback) {
super(baseBinding, currentUser, thread, callback);
this.binding = binding;
setItemView(binding.getRoot());
}
@Override
public void bindItem(final DirectItem item, final MessageDirection messageDirection) {
final DirectItemXma xma = item.getXma();
final XmaUrlInfo playableUrlInfo = xma.getPlayableUrlInfo();
final XmaUrlInfo previewUrlInfo = xma.getPreviewUrlInfo();
if (playableUrlInfo == null && previewUrlInfo == null) {
binding.ivAnimatedMessage.setController(null);
return;
}
final XmaUrlInfo urlInfo = playableUrlInfo != null ? playableUrlInfo : previewUrlInfo;
final String url = urlInfo.getUrl();
final NullSafePair widthHeight = NumberUtils.calculateWidthHeight(
urlInfo.getHeight(),
urlInfo.getWidth(),
mediaImageMaxHeight,
mediaImageMaxWidth
);
binding.ivAnimatedMessage.setVisibility(View.VISIBLE);
final ViewGroup.LayoutParams layoutParams = binding.ivAnimatedMessage.getLayoutParams();
final int width = widthHeight.first;
final int height = widthHeight.second;
layoutParams.width = width;
layoutParams.height = height;
binding.ivAnimatedMessage.requestLayout();
binding.ivAnimatedMessage.setController(Fresco.newDraweeControllerBuilder()
.setUri(url)
.setAutoPlayAnimations(true)
.build());
}
@Override
public int getSwipeDirection() {
return ItemTouchHelper.ACTION_STATE_IDLE;
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectPendingUserViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder.directmessages;
import android.graphics.drawable.Drawable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.util.Log;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.R;
import awais.instagrabber.adapters.DirectPendingUsersAdapter.PendingUser;
import awais.instagrabber.adapters.DirectPendingUsersAdapter.PendingUserCallback;
import awais.instagrabber.customviews.VerticalImageSpan;
import awais.instagrabber.databinding.LayoutDmPendingUserItemBinding;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.utils.Utils;
public class DirectPendingUserViewHolder extends RecyclerView.ViewHolder {
private static final String TAG = DirectPendingUserViewHolder.class.getSimpleName();
private final LayoutDmPendingUserItemBinding binding;
private final PendingUserCallback callback;
private final int drawableSize;
private VerticalImageSpan verifiedSpan;
public DirectPendingUserViewHolder(@NonNull final LayoutDmPendingUserItemBinding binding,
final PendingUserCallback callback) {
super(binding.getRoot());
this.binding = binding;
this.callback = callback;
drawableSize = Utils.convertDpToPx(24);
}
public void bind(final int position, final PendingUser pendingUser) {
if (pendingUser == null) return;
binding.getRoot().setOnClickListener(v -> {
if (callback == null) return;
callback.onClick(position, pendingUser);
});
setUsername(pendingUser);
binding.requester.setText(itemView.getResources().getString(R.string.added_by, pendingUser.getRequester()));
binding.profilePic.setImageURI(pendingUser.getUser().getProfilePicUrl());
if (pendingUser.isInProgress()) {
binding.approve.setVisibility(View.GONE);
binding.deny.setVisibility(View.GONE);
binding.progress.setVisibility(View.VISIBLE);
return;
}
binding.approve.setVisibility(View.VISIBLE);
binding.deny.setVisibility(View.VISIBLE);
binding.progress.setVisibility(View.GONE);
binding.approve.setOnClickListener(v -> {
if (callback == null) return;
callback.onApprove(position, pendingUser);
});
binding.deny.setOnClickListener(v -> {
if (callback == null) return;
callback.onDeny(position, pendingUser);
});
}
private void setUsername(final PendingUser pendingUser) {
final User user = pendingUser.getUser();
final SpannableStringBuilder sb = new SpannableStringBuilder(user.getUsername());
if (user.isVerified()) {
if (verifiedSpan == null) {
final Drawable verifiedDrawable = AppCompatResources.getDrawable(itemView.getContext(), R.drawable.verified);
if (verifiedDrawable != null) {
final Drawable drawable = verifiedDrawable.mutate();
drawable.setBounds(0, 0, drawableSize, drawableSize);
verifiedSpan = new VerticalImageSpan(drawable);
}
}
try {
if (verifiedSpan != null) {
sb.append(" ");
sb.setSpan(verifiedSpan, sb.length() - 1, sb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
} catch (Exception e) {
Log.e(TAG, "bind: ", e);
}
}
binding.username.setText(sb);
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectReactionViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder.directmessages;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.R;
import awais.instagrabber.adapters.DirectReactionsAdapter.OnReactionClickListener;
import awais.instagrabber.customviews.emoji.Emoji;
import awais.instagrabber.databinding.LayoutDmUserItemBinding;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItemEmojiReaction;
import awais.instagrabber.utils.emoji.EmojiParser;
public class DirectReactionViewHolder extends RecyclerView.ViewHolder {
private final LayoutDmUserItemBinding binding;
private final long viewerId;
private final String itemId;
private final OnReactionClickListener onReactionClickListener;
private final EmojiParser emojiParser;
public DirectReactionViewHolder(final LayoutDmUserItemBinding binding,
final long viewerId,
final String itemId,
final OnReactionClickListener onReactionClickListener) {
super(binding.getRoot());
this.binding = binding;
this.viewerId = viewerId;
this.itemId = itemId;
this.onReactionClickListener = onReactionClickListener;
binding.info.setVisibility(View.GONE);
binding.secondaryImage.setVisibility(View.VISIBLE);
emojiParser = EmojiParser.Companion.getInstance(itemView.getContext());
}
public void bind(final DirectItemEmojiReaction reaction,
@Nullable final User user) {
itemView.setOnClickListener(v -> {
if (onReactionClickListener == null) return;
onReactionClickListener.onReactionClick(itemId, reaction);
});
setUser(user);
setReaction(reaction);
}
private void setReaction(final DirectItemEmojiReaction reaction) {
final Emoji emoji = emojiParser.getEmoji(reaction.getEmoji());
if (emoji == null) {
binding.secondaryImage.setImageDrawable(null);
return;
}
binding.secondaryImage.setImageDrawable(emoji.getDrawable());
}
private void setUser(final User user) {
if (user == null) {
binding.fullName.setText("");
binding.username.setText("");
binding.profilePic.setImageURI((String) null);
return;
}
binding.fullName.setText(user.getFullName());
if (user.getPk() == viewerId) {
binding.username.setText(R.string.tap_to_remove);
} else {
binding.username.setText(user.getUsername());
}
binding.profilePic.setImageURI(user.getProfilePicUrl());
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectUserViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder.directmessages;
import android.graphics.drawable.Drawable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.util.Log;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.R;
import awais.instagrabber.adapters.DirectUsersAdapter.OnDirectUserClickListener;
import awais.instagrabber.adapters.DirectUsersAdapter.OnDirectUserLongClickListener;
import awais.instagrabber.customviews.VerticalImageSpan;
import awais.instagrabber.databinding.LayoutDmUserItemBinding;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.utils.Utils;
public class DirectUserViewHolder extends RecyclerView.ViewHolder {
private static final String TAG = DirectUserViewHolder.class.getSimpleName();
private final LayoutDmUserItemBinding binding;
private final OnDirectUserClickListener onClickListener;
private final OnDirectUserLongClickListener onLongClickListener;
private final int drawableSize;
private VerticalImageSpan verifiedSpan;
public DirectUserViewHolder(@NonNull final LayoutDmUserItemBinding binding,
final OnDirectUserClickListener onClickListener,
final OnDirectUserLongClickListener onLongClickListener) {
super(binding.getRoot());
this.binding = binding;
this.onClickListener = onClickListener;
this.onLongClickListener = onLongClickListener;
drawableSize = Utils.convertDpToPx(24);
}
public void bind(final int position,
final User user,
final boolean isAdmin,
final boolean isInviter,
final boolean showSelection,
final boolean isSelected) {
if (user == null) return;
binding.getRoot().setOnClickListener(v -> {
if (onClickListener == null) return;
onClickListener.onClick(position, user, isSelected);
});
binding.getRoot().setOnLongClickListener(v -> {
if (onLongClickListener == null) return false;
return onLongClickListener.onLongClick(position, user);
});
setFullName(user);
binding.username.setText(user.getUsername());
binding.profilePic.setImageURI(user.getProfilePicUrl());
setInfo(isAdmin, isInviter);
setSelection(showSelection, isSelected);
}
private void setFullName(final User user) {
final SpannableStringBuilder sb = new SpannableStringBuilder(user.getFullName());
if (user.isVerified()) {
if (verifiedSpan == null) {
final Drawable verifiedDrawable = AppCompatResources.getDrawable(itemView.getContext(), R.drawable.verified);
if (verifiedDrawable != null) {
final Drawable drawable = verifiedDrawable.mutate();
drawable.setBounds(0, 0, drawableSize, drawableSize);
verifiedSpan = new VerticalImageSpan(drawable);
}
}
try {
if (verifiedSpan != null) {
sb.append(" ");
sb.setSpan(verifiedSpan, sb.length() - 1, sb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
} catch (Exception e) {
Log.e(TAG, "bind: ", e);
}
}
binding.fullName.setText(sb);
}
private void setInfo(final boolean isAdmin, final boolean isInviter) {
if (!isAdmin && !isInviter) {
binding.info.setVisibility(View.GONE);
return;
}
if (isAdmin) {
binding.info.setText(R.string.admin);
return;
}
binding.info.setText(R.string.inviter);
}
private void setSelection(final boolean showSelection, final boolean isSelected) {
binding.select.setVisibility(showSelection ? View.VISIBLE : View.GONE);
binding.getRoot().setSelected(isSelected);
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/RecipientThreadViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder.directmessages;
import android.content.res.Resources;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
import awais.instagrabber.R;
import awais.instagrabber.adapters.UserSearchResultsAdapter.OnRecipientClickListener;
import awais.instagrabber.databinding.LayoutDmUserItemBinding;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
import awais.instagrabber.repositories.responses.directmessages.RankedRecipient;
public class RecipientThreadViewHolder extends RecyclerView.ViewHolder {
private static final String TAG = RecipientThreadViewHolder.class.getSimpleName();
private final LayoutDmUserItemBinding binding;
private final OnRecipientClickListener onThreadClickListener;
private final float translateAmount;
public RecipientThreadViewHolder(@NonNull final LayoutDmUserItemBinding binding,
final OnRecipientClickListener onThreadClickListener) {
super(binding.getRoot());
this.binding = binding;
this.onThreadClickListener = onThreadClickListener;
binding.info.setVisibility(View.GONE);
final Resources resources = itemView.getResources();
final int avatarSize = resources.getDimensionPixelSize(R.dimen.dm_inbox_avatar_size);
translateAmount = ((float) avatarSize) / 7;
}
public void bind(final int position,
final DirectThread thread,
final boolean showSelection,
final boolean isSelected) {
if (thread == null || thread.getUsers().size() == 0) return;
binding.getRoot().setOnClickListener(v -> {
if (onThreadClickListener == null) return;
onThreadClickListener.onClick(position, RankedRecipient.of(thread), isSelected);
});
binding.fullName.setText(thread.getThreadTitle());
setUsername(thread);
setProfilePic(thread);
setSelection(showSelection, isSelected);
}
private void setProfilePic(final DirectThread thread) {
final List users = thread.getUsers();
binding.profilePic.setImageURI(users.get(0).getProfilePicUrl());
binding.profilePic.setScaleX(1);
binding.profilePic.setScaleY(1);
binding.profilePic.setTranslationX(0);
binding.profilePic.setTranslationY(0);
if (users.size() > 1) {
binding.profilePic2.setVisibility(View.VISIBLE);
binding.profilePic2.setImageURI(users.get(1).getProfilePicUrl());
binding.profilePic2.setTranslationX(translateAmount);
binding.profilePic2.setTranslationY(translateAmount);
final float scaleAmount = 0.75f;
binding.profilePic2.setScaleX(scaleAmount);
binding.profilePic2.setScaleY(scaleAmount);
binding.profilePic.setScaleX(scaleAmount);
binding.profilePic.setScaleY(scaleAmount);
binding.profilePic.setTranslationX(-translateAmount);
binding.profilePic.setTranslationY(-translateAmount);
return;
}
binding.profilePic2.setVisibility(View.GONE);
}
private void setUsername(final DirectThread thread) {
if (thread.isGroup()) {
binding.username.setVisibility(View.GONE);
return;
}
binding.username.setVisibility(View.VISIBLE);
// for a non-group thread, the thread title is the username so set the full name in the username text view
binding.username.setText(thread.getUsers().get(0).getFullName());
}
private void setSelection(final boolean showSelection, final boolean isSelected) {
binding.select.setVisibility(showSelection ? View.VISIBLE : View.GONE);
binding.getRoot().setSelected(isSelected);
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedItemViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder.feed;
import android.graphics.drawable.Drawable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.transition.TransitionManager;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RelativeLayout;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.R;
import awais.instagrabber.adapters.FeedAdapterV2;
import awais.instagrabber.customviews.VerticalImageSpan;
import awais.instagrabber.databinding.ItemFeedTopBinding;
import awais.instagrabber.databinding.LayoutPostViewBottomBinding;
import awais.instagrabber.models.enums.MediaItemType;
import awais.instagrabber.repositories.responses.Caption;
import awais.instagrabber.repositories.responses.Location;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils;
import static android.text.TextUtils.TruncateAt.END;
public abstract class FeedItemViewHolder extends RecyclerView.ViewHolder {
public static final int MAX_LINES_COLLAPSED = 5;
private final ItemFeedTopBinding topBinding;
private final LayoutPostViewBottomBinding bottomBinding;
private final ViewGroup bottomFrame;
private final FeedAdapterV2.FeedItemCallback feedItemCallback;
public FeedItemViewHolder(@NonNull final ViewGroup root,
final FeedAdapterV2.FeedItemCallback feedItemCallback) {
super(root);
this.bottomFrame = root;
this.topBinding = ItemFeedTopBinding.bind(root);
this.bottomBinding = LayoutPostViewBottomBinding.bind(root);
this.feedItemCallback = feedItemCallback;
}
public void bind(final Media media) {
if (media == null) {
return;
}
setupProfilePic(media);
bottomBinding.date.setText(media.getDate());
setupComments(media);
setupCaption(media);
setupActions(media);
if (media.getType() != MediaItemType.MEDIA_TYPE_SLIDER) {
bottomBinding.download.setOnClickListener(v ->
feedItemCallback.onDownloadClick(media, -1, bottomBinding.download)
);
}
bindItem(media);
bottomFrame.post(() -> setupLocation(media));
}
private void setupComments(@NonNull final Media feedModel) {
final long commentsCount = feedModel.getCommentCount();
bottomBinding.commentsCount.setText(String.valueOf(commentsCount));
bottomBinding.comment.setOnClickListener(v -> feedItemCallback.onCommentsClick(feedModel));
}
private void setupProfilePic(@NonNull final Media media) {
final User user = media.getUser();
if (user == null) {
topBinding.profilePic.setVisibility(View.GONE);
topBinding.title.setVisibility(View.GONE);
topBinding.subtitle.setVisibility(View.GONE);
return;
}
topBinding.profilePic.setOnClickListener(v -> feedItemCallback.onProfilePicClick(media));
topBinding.profilePic.setImageURI(user.getProfilePicUrl());
setupTitle(media);
}
private void setupTitle(@NonNull final Media media) {
// final int titleLen = profileModel.getUsername().length() + 1;
// final SpannableString spannableString = new SpannableString();
// spannableString.setSpan(new CommentMentionClickSpan(), 0, titleLen, 0);
final User user = media.getUser();
if (user == null) return;
setUsername(user);
topBinding.title.setOnClickListener(v -> feedItemCallback.onNameClick(media));
final String fullName = user.getFullName();
if (TextUtils.isEmpty(fullName)) {
topBinding.subtitle.setVisibility(View.GONE);
} else {
topBinding.subtitle.setVisibility(View.VISIBLE);
topBinding.subtitle.setText(fullName);
}
topBinding.subtitle.setOnClickListener(v -> feedItemCallback.onNameClick(media));
}
private void setupCaption(final Media media) {
bottomBinding.caption.clearOnMentionClickListeners();
bottomBinding.caption.clearOnHashtagClickListeners();
bottomBinding.caption.clearOnURLClickListeners();
bottomBinding.caption.clearOnEmailClickListeners();
final Caption caption = media.getCaption();
if (caption == null) {
bottomBinding.caption.setVisibility(View.GONE);
return;
}
final CharSequence postCaption = caption.getText();
final boolean captionEmpty = TextUtils.isEmpty(postCaption);
bottomBinding.caption.setVisibility(captionEmpty ? View.GONE : View.VISIBLE);
if (captionEmpty) return;
bottomBinding.caption.setText(postCaption);
bottomBinding.caption.setMaxLines(MAX_LINES_COLLAPSED);
bottomBinding.caption.setEllipsize(END);
bottomBinding.caption.setOnClickListener(v -> bottomFrame.post(() -> {
TransitionManager.beginDelayedTransition(bottomFrame);
if (bottomBinding.caption.getMaxLines() == MAX_LINES_COLLAPSED) {
bottomBinding.caption.setMaxLines(Integer.MAX_VALUE);
bottomBinding.caption.setEllipsize(null);
return;
}
bottomBinding.caption.setMaxLines(MAX_LINES_COLLAPSED);
bottomBinding.caption.setEllipsize(END);
}));
bottomBinding.caption.addOnMentionClickListener(autoLinkItem -> feedItemCallback.onMentionClick(autoLinkItem.getOriginalText()));
bottomBinding.caption.addOnHashtagListener(autoLinkItem -> feedItemCallback.onHashtagClick(autoLinkItem.getOriginalText()));
bottomBinding.caption.addOnEmailClickListener(autoLinkItem -> feedItemCallback.onEmailClick(autoLinkItem.getOriginalText()));
bottomBinding.caption.addOnURLClickListener(autoLinkItem -> feedItemCallback.onURLClick(autoLinkItem.getOriginalText()));
}
private void setupLocation(@NonNull final Media media) {
final Location location = media.getLocation();
if (location == null) {
topBinding.location.setVisibility(View.GONE);
} else {
final String locationName = location.getName();
if (TextUtils.isEmpty(locationName)) {
topBinding.location.setVisibility(View.GONE);
} else {
topBinding.location.setVisibility(View.VISIBLE);
topBinding.location.setText(locationName);
topBinding.location.setOnClickListener(v -> feedItemCallback.onLocationClick(media));
}
}
}
private void setupActions(@NonNull final Media media) {
// temporary - to be set up later
bottomBinding.like.setVisibility(View.GONE);
bottomBinding.save.setVisibility(View.GONE);
bottomBinding.translate.setVisibility(View.GONE);
bottomBinding.share.setVisibility(View.GONE);
}
private void setUsername(final User user) {
final SpannableStringBuilder sb = new SpannableStringBuilder(user.getUsername());
final int drawableSize = Utils.convertDpToPx(24);
if (user.isVerified()) {
final Drawable verifiedDrawable = itemView.getResources().getDrawable(R.drawable.verified);
VerticalImageSpan verifiedSpan = null;
if (verifiedDrawable != null) {
final Drawable drawable = verifiedDrawable.mutate();
drawable.setBounds(0, 0, drawableSize, drawableSize);
verifiedSpan = new VerticalImageSpan(drawable);
}
try {
if (verifiedSpan != null) {
sb.append(" ");
sb.setSpan(verifiedSpan, sb.length() - 1, sb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
} catch (Exception e) {
Log.e("FeedItemViewHolder", "setUsername: ", e);
}
}
topBinding.title.setText(sb);
}
public abstract void bindItem(final Media media);
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedPhotoViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder.feed;
import android.net.Uri;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.NonNull;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.drawee.drawable.ScalingUtils;
import com.facebook.drawee.generic.GenericDraweeHierarchy;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.imagepipeline.request.ImageRequestBuilder;
import awais.instagrabber.adapters.FeedAdapterV2;
import awais.instagrabber.databinding.ItemFeedPhotoBinding;
import awais.instagrabber.databinding.LayoutPostViewBottomBinding;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.utils.ResponseBodyUtils;
import awais.instagrabber.utils.TextUtils;
public class FeedPhotoViewHolder extends FeedItemViewHolder {
private static final String TAG = "FeedPhotoViewHolder";
private final ItemFeedPhotoBinding binding;
private final FeedAdapterV2.FeedItemCallback feedItemCallback;
public FeedPhotoViewHolder(@NonNull final ItemFeedPhotoBinding binding,
final FeedAdapterV2.FeedItemCallback feedItemCallback) {
super(binding.getRoot(), feedItemCallback);
this.binding = binding;
this.feedItemCallback = feedItemCallback;
final LayoutPostViewBottomBinding bottom = LayoutPostViewBottomBinding.bind(binding.getRoot());
bottom.viewsCount.setVisibility(View.GONE);
// binding.itemFeedBottom.btnMute.setVisibility(View.GONE);
binding.imageViewer.setAllowTouchInterceptionWhileZoomed(false);
final GenericDraweeHierarchy hierarchy = new GenericDraweeHierarchyBuilder(itemView.getContext().getResources())
.setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER)
.build();
binding.imageViewer.setHierarchy(hierarchy);
}
@Override
public void bindItem(final Media media) {
if (media == null) return;
binding.getRoot().post(() -> {
setDimensions(media);
final String thumbnailUrl = ResponseBodyUtils.getThumbUrl(media);
String url = ResponseBodyUtils.getImageUrl(media);
if (TextUtils.isEmpty(url)) url = thumbnailUrl;
final ImageRequest requestBuilder = ImageRequestBuilder.newBuilderWithSource(Uri.parse(url))
// .setLocalThumbnailPreviewsEnabled(true)
// .setProgressiveRenderingEnabled(true)
.build();
binding.imageViewer.setController(Fresco.newDraweeControllerBuilder()
.setImageRequest(requestBuilder)
.setOldController(binding.imageViewer.getController())
.setLowResImageRequest(ImageRequest.fromUri(thumbnailUrl))
.build());
binding.imageViewer.setTapListener(new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onSingleTapConfirmed(final MotionEvent e) {
if (feedItemCallback != null) {
feedItemCallback.onPostClick(media);
return true;
}
return false;
}
});
});
}
private void setDimensions(final Media feedModel) {
final float aspectRatio = (float) feedModel.getOriginalWidth() / feedModel.getOriginalHeight();
binding.imageViewer.setAspectRatio(aspectRatio);
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedSliderViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder.feed;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import androidx.annotation.NonNull;
import androidx.viewpager2.widget.ViewPager2;
import java.util.List;
import awais.instagrabber.adapters.FeedAdapterV2;
import awais.instagrabber.adapters.SliderCallbackAdapter;
import awais.instagrabber.adapters.SliderItemsAdapter;
import awais.instagrabber.databinding.ItemFeedSliderBinding;
import awais.instagrabber.databinding.LayoutPostViewBottomBinding;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.utils.NumberUtils;
import awais.instagrabber.utils.Utils;
public class FeedSliderViewHolder extends FeedItemViewHolder {
private static final String TAG = "FeedSliderViewHolder";
// private static final boolean shouldAutoPlay = settingsHelper.getBoolean(Constants.AUTOPLAY_VIDEOS);
private final ItemFeedSliderBinding binding;
private final FeedAdapterV2.FeedItemCallback feedItemCallback;
private final LayoutPostViewBottomBinding bottom;
public FeedSliderViewHolder(@NonNull final ItemFeedSliderBinding binding,
final FeedAdapterV2.FeedItemCallback feedItemCallback) {
super(binding.getRoot(), feedItemCallback);
this.binding = binding;
this.feedItemCallback = feedItemCallback;
bottom = LayoutPostViewBottomBinding.bind(binding.getRoot());
bottom.viewsCount.setVisibility(View.GONE);
// bottom.btnMute.setVisibility(View.GONE);
final ViewGroup.LayoutParams layoutParams = binding.mediaList.getLayoutParams();
layoutParams.height = Utils.displayMetrics.widthPixels + 1;
binding.mediaList.setLayoutParams(layoutParams);
// final Context context = binding.getRoot().getContext();
}
@Override
public void bindItem(final Media feedModel) {
final List sliderItems = feedModel.getCarouselMedia();
final int sliderItemLen = sliderItems != null ? sliderItems.size() : 0;
if (sliderItemLen <= 0) return;
final String text = "1/" + sliderItemLen;
binding.mediaCounter.setText(text);
binding.mediaList.setOffscreenPageLimit(1);
final SliderItemsAdapter adapter = new SliderItemsAdapter(false, new SliderCallbackAdapter() {
@Override
public void onItemClicked(final int position, final Media media, final View view) {
feedItemCallback.onSliderClick(feedModel, position);
}
});
binding.mediaList.setAdapter(adapter);
binding.mediaList.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageSelected(final int position) {
if (position >= sliderItemLen) return;
final String text = (position + 1) + "/" + sliderItemLen;
binding.mediaCounter.setText(text);
setDimensions(binding.mediaList, sliderItems.get(position));
bottom.download.setOnClickListener(v ->
feedItemCallback.onDownloadClick(feedModel, position, bottom.download)
);
}
});
setDimensions(binding.mediaList, sliderItems.get(0));
bottom.download.setOnClickListener(v ->
feedItemCallback.onDownloadClick(feedModel, 0, bottom.download)
);
adapter.submitList(sliderItems);
}
private void setDimensions(final View view, final Media model) {
final ViewGroup.LayoutParams layoutParams = binding.mediaList.getLayoutParams();
int requiredWidth = layoutParams.width;
if (requiredWidth <= 0) {
final ViewTreeObserver.OnPreDrawListener preDrawListener = new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
view.getViewTreeObserver().removeOnPreDrawListener(this);
setLayoutParamDimens(binding.mediaList, model);
return true;
}
};
view.getViewTreeObserver().addOnPreDrawListener(preDrawListener);
return;
}
setLayoutParamDimens(binding.mediaList, model);
}
private void setLayoutParamDimens(final View view, final Media model) {
final int requiredWidth = view.getMeasuredWidth();
final ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
final int spanHeight = NumberUtils.getResultingHeight(requiredWidth, model.getOriginalHeight(), model.getOriginalWidth());
layoutParams.height = spanHeight == 0 ? requiredWidth + 1 : spanHeight;
view.requestLayout();
}
// private void autoPlay(final int position) {
// if (!shouldAutoPlay) {
// return;
// }
// final ChildMediaItemsAdapter adapter = (ChildMediaItemsAdapter) binding.mediaList.getAdapter();
// if (adapter == null) {
// return;
// }
// final ViewerPostModel sliderItem = adapter.getItemAtPosition(position);
// if (sliderItem.getItemType() != MediaItemType.MEDIA_TYPE_VIDEO) {
// return;
// }
// final ViewSwitcher viewSwitcher = (ViewSwitcher) binding.mediaList.getChildAt(position);
// loadPlayer(binding.getRoot().getContext(),
// position, sliderItem.getDisplayUrl(),
// viewSwitcher,
// cacheDataSourceFactory != null ? cacheDataSourceFactory : dataSourceFactory,
// playerChangeListener);
// }
// public void startPlayingVideo() {
// final int playerPosition = 0;
// autoPlay(playerPosition);
// }
//
// public void stopPlayingVideo() {
// if (pagerPlayer == null) {
// return;
// }
// pagerPlayer.setPlayWhenReady(false);
// }
// private interface PlayerChangeListener {
// void playerChanged(final int position, final SimpleExoPlayer player);
// }
//
// private static void loadPlayer(final Context context,
// final int position,
// final String displayUrl,
// final ViewSwitcher viewSwitcher,
// final DataSource.Factory factory,
// final PlayerChangeListener playerChangeListener) {
// if (viewSwitcher == null) {
// return;
// }
// SimpleExoPlayer player = (SimpleExoPlayer) viewSwitcher.getTag();
// if (player != null) {
// player.setPlayWhenReady(true);
// return;
// }
// player = new SimpleExoPlayer.Builder(context).build();
// final PlayerView playerView = (PlayerView) viewSwitcher.getChildAt(1);
// playerView.setPlayer(player);
// if (viewSwitcher.getDisplayedChild() == 0) {
// viewSwitcher.showNext();
// }
// playerView.setControllerShowTimeoutMs(1000);
// float vol = settingsHelper.getBoolean(Constants.MUTED_VIDEOS) ? 0f : 1f;
// if (vol == 0f && Utils.sessionVolumeFull) vol = 1f;
// player.setVolume(vol);
// player.setPlayWhenReady(Utils.settingsHelper.getBoolean(Constants.AUTOPLAY_VIDEOS));
// final MediaItem mediaItem = MediaItem.fromUri(displayUrl);
// final ProgressiveMediaSource mediaSource = new ProgressiveMediaSource.Factory(factory).createMediaSource(mediaItem);
// player.setRepeatMode(Player.REPEAT_MODE_ALL);
// player.setMediaSource(mediaSource);
// player.prepare();
// player.setVolume(vol);
// playerChangeListener.playerChanged(position, player);
// viewSwitcher.setTag(player);
// }
}
================================================
FILE: app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedVideoViewHolder.java
================================================
package awais.instagrabber.adapters.viewholder.feed;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory;
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
import java.util.List;
import awais.instagrabber.R;
import awais.instagrabber.adapters.FeedAdapterV2;
import awais.instagrabber.customviews.VideoPlayerCallbackAdapter;
import awais.instagrabber.customviews.VideoPlayerViewHelper;
import awais.instagrabber.databinding.ItemFeedVideoBinding;
import awais.instagrabber.databinding.LayoutPostViewBottomBinding;
import awais.instagrabber.databinding.LayoutVideoPlayerWithThumbnailBinding;
import awais.instagrabber.fragments.settings.PreferenceKeys;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.MediaCandidate;
import awais.instagrabber.utils.NullSafePair;
import awais.instagrabber.utils.NumberUtils;
import awais.instagrabber.utils.ResponseBodyUtils;
import awais.instagrabber.utils.Utils;
import static awais.instagrabber.utils.Utils.settingsHelper;
public class FeedVideoViewHolder extends FeedItemViewHolder {
private static final String TAG = "FeedVideoViewHolder";
private final ItemFeedVideoBinding binding;
private final FeedAdapterV2.FeedItemCallback feedItemCallback;
private final Handler handler;
private final DefaultDataSourceFactory dataSourceFactory;
private final LayoutPostViewBottomBinding bottom;
private CacheDataSourceFactory cacheDataSourceFactory;
private Media media;
// private final Runnable loadRunnable = new Runnable() {
// @Override
// public void run() {
// // loadPlayer(feedModel);
// }
// };
public FeedVideoViewHolder(@NonNull final ItemFeedVideoBinding binding,
final FeedAdapterV2.FeedItemCallback feedItemCallback) {
super(binding.getRoot(), feedItemCallback);
bottom = LayoutPostViewBottomBinding.bind(binding.getRoot());
this.binding = binding;
this.feedItemCallback = feedItemCallback;
bottom.viewsCount.setVisibility(View.VISIBLE);
handler = new Handler(Looper.getMainLooper());
final Context context = binding.getRoot().getContext();
dataSourceFactory = new DefaultDataSourceFactory(context, "instagram");
final SimpleCache simpleCache = Utils.getSimpleCacheInstance(context);
if (simpleCache != null) {
cacheDataSourceFactory = new CacheDataSourceFactory(simpleCache, dataSourceFactory);
}
}
@Override
public void bindItem(final Media media) {
// Log.d(TAG, "Binding post: " + feedModel.getPostId());
this.media = media;
final String viewCount = itemView.getResources().getQuantityString(R.plurals.views_count, (int) media.getViewCount(), media.getViewCount());
bottom.viewsCount.setText(viewCount);
final LayoutVideoPlayerWithThumbnailBinding videoPost =
LayoutVideoPlayerWithThumbnailBinding.inflate(LayoutInflater.from(itemView.getContext()), binding.getRoot(), false);
final ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) videoPost.getRoot().getLayoutParams();
final NullSafePair widthHeight = NumberUtils.calculateWidthHeight(media.getOriginalHeight(),
media.getOriginalWidth(),
(int) (Utils.displayMetrics.heightPixels * 0.8),
Utils.displayMetrics.widthPixels);
layoutParams.width = ConstraintLayout.LayoutParams.MATCH_PARENT;
layoutParams.height = widthHeight.second;
final View postView = videoPost.getRoot();
binding.postContainer.addView(postView);
final float vol = settingsHelper.getBoolean(PreferenceKeys.MUTED_VIDEOS) ? 0f : 1f;
final VideoPlayerViewHelper.VideoPlayerCallback videoPlayerCallback = new VideoPlayerCallbackAdapter() {
@Override
public void onThumbnailClick() {
feedItemCallback.onPostClick(media);
}
@Override
public void onPlayerViewLoaded() {
final ViewGroup.LayoutParams layoutParams = videoPost.playerView.getLayoutParams();
final int requiredWidth = Utils.displayMetrics.widthPixels;
final int resultingHeight = NumberUtils.getResultingHeight(requiredWidth, media.getOriginalHeight(), media.getOriginalWidth());
layoutParams.width = requiredWidth;
layoutParams.height = resultingHeight;
videoPost.playerView.requestLayout();
}
};
final float aspectRatio = (float) media.getOriginalWidth() / media.getOriginalHeight();
String videoUrl = null;
final List videoVersions = media.getVideoVersions();
if (videoVersions != null && !videoVersions.isEmpty()) {
final MediaCandidate videoVersion = videoVersions.get(0);
videoUrl = videoVersion.getUrl();
}
final VideoPlayerViewHelper videoPlayerViewHelper = new VideoPlayerViewHelper(binding.getRoot().getContext(),
videoPost,
videoUrl,
vol,
aspectRatio,
ResponseBodyUtils.getThumbUrl(media),
false,
// null,
videoPlayerCallback);
videoPost.thumbnail.post(() -> {
if (media.getOriginalHeight() > 0.8 * Utils.displayMetrics.heightPixels) {
final ViewGroup.LayoutParams tLayoutParams = videoPost.thumbnail.getLayoutParams();
tLayoutParams.height = (int) (0.8 * Utils.displayMetrics.heightPixels);
videoPost.thumbnail.requestLayout();
}
});
}
public Media getCurrentFeedModel() {
return media;
}
// public void stopPlaying() {
// // Log.d(TAG, "Stopping post: " + feedModel.getPostId() + ", player: " + player + ", player.isPlaying: " + (player != null && player.isPlaying()));
// handler.removeCallbacks(loadRunnable);
// if (player != null) {
// player.release();
// }
// if (videoPost.root.getDisplayedChild() == 1) {
// videoPost.root.showPrevious();
// }
// }
//
// public void startPlaying() {
// handler.removeCallbacks(loadRunnable);
// handler.postDelayed(loadRunnable, 800);
// }
}
================================================
FILE: app/src/main/java/awais/instagrabber/animations/CubicBezierInterpolator.java
================================================
package awais.instagrabber.animations;
import android.graphics.PointF;
import android.view.animation.Interpolator;
public class CubicBezierInterpolator implements Interpolator {
public static final CubicBezierInterpolator DEFAULT = new CubicBezierInterpolator(0.25, 0.1, 0.25, 1);
public static final CubicBezierInterpolator EASE_OUT = new CubicBezierInterpolator(0, 0, .58, 1);
public static final CubicBezierInterpolator EASE_OUT_QUINT = new CubicBezierInterpolator(.23, 1, .32, 1);
public static final CubicBezierInterpolator EASE_IN = new CubicBezierInterpolator(.42, 0, 1, 1);
public static final CubicBezierInterpolator EASE_BOTH = new CubicBezierInterpolator(.42, 0, .58, 1);
protected PointF start;
protected PointF end;
protected PointF a = new PointF();
protected PointF b = new PointF();
protected PointF c = new PointF();
public CubicBezierInterpolator(PointF start, PointF end) throws IllegalArgumentException {
if (start.x < 0 || start.x > 1) {
throw new IllegalArgumentException("startX value must be in the range [0, 1]");
}
if (end.x < 0 || end.x > 1) {
throw new IllegalArgumentException("endX value must be in the range [0, 1]");
}
this.start = start;
this.end = end;
}
public CubicBezierInterpolator(float startX, float startY, float endX, float endY) {
this(new PointF(startX, startY), new PointF(endX, endY));
}
public CubicBezierInterpolator(double startX, double startY, double endX, double endY) {
this((float) startX, (float) startY, (float) endX, (float) endY);
}
@Override
public float getInterpolation(float time) {
return getBezierCoordinateY(getXForTime(time));
}
protected float getBezierCoordinateY(float time) {
c.y = 3 * start.y;
b.y = 3 * (end.y - start.y) - c.y;
a.y = 1 - c.y - b.y;
return time * (c.y + time * (b.y + time * a.y));
}
protected float getXForTime(float time) {
float x = time;
float z;
for (int i = 1; i < 14; i++) {
z = getBezierCoordinateX(x) - time;
if (Math.abs(z) < 1e-3) {
break;
}
x -= z / getXDerivate(x);
}
return x;
}
private float getXDerivate(float t) {
return c.x + t * (2 * b.x + 3 * a.x * t);
}
private float getBezierCoordinateX(float time) {
c.x = 3 * start.x;
b.x = 3 * (end.x - start.x) - c.x;
a.x = 1 - c.x - b.x;
return time * (c.x + time * (b.x + time * a.x));
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/animations/FabAnimation.java
================================================
package awais.instagrabber.animations;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.view.View;
// https://medium.com/better-programming/animated-fab-button-with-more-options-2dcf7118fff6
public class FabAnimation {
public static boolean rotateFab(final View v, boolean rotate) {
v.animate().setDuration(200)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
}
})
.rotation(rotate ? 135f : 0f);
return rotate;
}
public static void showIn(final View v) {
v.setVisibility(View.VISIBLE);
v.setAlpha(0f);
v.setTranslationY(v.getHeight());
v.animate()
.setDuration(200)
.translationY(0)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
}
})
.alpha(1f)
.start();
}
public static void showOut(final View v) {
v.setVisibility(View.VISIBLE);
v.setAlpha(1f);
v.setTranslationY(0);
v.animate()
.setDuration(200)
.translationY(v.getHeight())
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
v.setVisibility(View.GONE);
super.onAnimationEnd(animation);
}
}).alpha(0f)
.start();
}
public static void init(final View v) {
v.setVisibility(View.GONE);
v.setTranslationY(v.getHeight());
v.setAlpha(0f);
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/animations/ResizeAnimation.java
================================================
package awais.instagrabber.animations;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.Transformation;
public class ResizeAnimation extends Animation {
private static final String TAG = "ResizeAnimation";
final View view;
final int startHeight;
final int targetHeight;
final int startWidth;
final int targetWidth;
public ResizeAnimation(final View view,
final int startHeight,
final int startWidth,
final int targetHeight,
final int targetWidth) {
this.view = view;
this.startHeight = startHeight;
this.targetHeight = targetHeight;
this.startWidth = startWidth;
this.targetWidth = targetWidth;
}
@Override
protected void applyTransformation(final float interpolatedTime, final Transformation t) {
// Log.d(TAG, "applyTransformation: interpolatedTime: " + interpolatedTime);
view.getLayoutParams().height = (int) (startHeight + (targetHeight - startHeight) * interpolatedTime);
view.getLayoutParams().width = (int) (startWidth + (targetWidth - startWidth) * interpolatedTime);
view.requestLayout();
}
@Override
public void initialize(final int width, final int height, final int parentWidth, final int parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
}
@Override
public boolean willChangeBounds() {
return true;
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/animations/RevealOutlineAnimation.java
================================================
package awais.instagrabber.animations;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.graphics.Outline;
import android.graphics.Rect;
import android.view.View;
import android.view.ViewOutlineProvider;
/**
* A {@link ViewOutlineProvider} that has helper functions to create reveal animations.
* This class should be extended so that subclasses can define the reveal shape as the
* animation progresses from 0 to 1.
*/
public abstract class RevealOutlineAnimation extends ViewOutlineProvider {
protected Rect mOutline;
protected float mOutlineRadius;
public RevealOutlineAnimation() {
mOutline = new Rect();
}
/**
* Returns whether elevation should be removed for the duration of the reveal animation.
*/
abstract boolean shouldRemoveElevationDuringAnimation();
/**
* Sets the progress, from 0 to 1, of the reveal animation.
*/
abstract void setProgress(float progress);
public ValueAnimator createRevealAnimator(final View revealView, boolean isReversed) {
ValueAnimator va =
isReversed ? ValueAnimator.ofFloat(1f, 0f) : ValueAnimator.ofFloat(0f, 1f);
final float elevation = revealView.getElevation();
va.addListener(new AnimatorListenerAdapter() {
private boolean mIsClippedToOutline;
private ViewOutlineProvider mOldOutlineProvider;
public void onAnimationStart(Animator animation) {
mIsClippedToOutline = revealView.getClipToOutline();
mOldOutlineProvider = revealView.getOutlineProvider();
revealView.setOutlineProvider(RevealOutlineAnimation.this);
revealView.setClipToOutline(true);
if (shouldRemoveElevationDuringAnimation()) {
revealView.setTranslationZ(-elevation);
}
}
public void onAnimationEnd(Animator animation) {
revealView.setOutlineProvider(mOldOutlineProvider);
revealView.setClipToOutline(mIsClippedToOutline);
if (shouldRemoveElevationDuringAnimation()) {
revealView.setTranslationZ(0);
}
}
});
va.addUpdateListener(v -> {
float progress = (Float) v.getAnimatedValue();
setProgress(progress);
revealView.invalidateOutline();
});
return va;
}
@Override
public void getOutline(View v, Outline outline) {
outline.setRoundRect(mOutline, mOutlineRadius);
}
public float getRadius() {
return mOutlineRadius;
}
public void getOutline(Rect out) {
out.set(mOutline);
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/animations/RoundedRectRevealOutlineProvider.java
================================================
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package awais.instagrabber.animations;
import android.graphics.Rect;
/**
* A {@link RevealOutlineAnimation} that provides an outline that interpolates between two radii
* and two {@link Rect}s.
*
* An example usage of this provider is an outline that starts out as a circle and ends
* as a rounded rectangle.
*/
public class RoundedRectRevealOutlineProvider extends RevealOutlineAnimation {
private final float mStartRadius;
private final float mEndRadius;
private final Rect mStartRect;
private final Rect mEndRect;
public RoundedRectRevealOutlineProvider(float startRadius, float endRadius, Rect startRect, Rect endRect) {
mStartRadius = startRadius;
mEndRadius = endRadius;
mStartRect = startRect;
mEndRect = endRect;
}
@Override
public boolean shouldRemoveElevationDuringAnimation() {
return false;
}
@Override
public void setProgress(float progress) {
mOutlineRadius = (1 - progress) * mStartRadius + progress * mEndRadius;
mOutline.left = (int) ((1 - progress) * mStartRect.left + progress * mEndRect.left);
mOutline.top = (int) ((1 - progress) * mStartRect.top + progress * mEndRect.top);
mOutline.right = (int) ((1 - progress) * mStartRect.right + progress * mEndRect.right);
mOutline.bottom = (int) ((1 - progress) * mStartRect.bottom + progress * mEndRect.bottom);
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/animations/ScaleAnimation.java
================================================
package awais.instagrabber.animations;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
public class ScaleAnimation {
private final View view;
public ScaleAnimation(View view) {
this.view = view;
}
public void start() {
AnimatorSet set = new AnimatorSet();
ObjectAnimator scaleY = ObjectAnimator.ofFloat(view, "scaleY", 2.0f);
ObjectAnimator scaleX = ObjectAnimator.ofFloat(view, "scaleX", 2.0f);
set.setDuration(150);
set.setInterpolator(new AccelerateDecelerateInterpolator());
set.playTogether(scaleY, scaleX);
set.start();
}
public void stop() {
AnimatorSet set = new AnimatorSet();
ObjectAnimator scaleY = ObjectAnimator.ofFloat(view, "scaleY", 1.0f);
// scaleY.setDuration(250);
// scaleY.setInterpolator(new DecelerateInterpolator());
ObjectAnimator scaleX = ObjectAnimator.ofFloat(view, "scaleX", 1.0f);
// scaleX.setDuration(250);
// scaleX.setInterpolator(new DecelerateInterpolator());
set.setDuration(150);
set.setInterpolator(new AccelerateDecelerateInterpolator());
set.playTogether(scaleY, scaleX);
set.start();
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/asyncs/DiscoverPostFetchService.java
================================================
package awais.instagrabber.asyncs;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import awais.instagrabber.customviews.helpers.PostFetcher;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.discover.TopicalExploreFeedResponse;
import awais.instagrabber.repositories.responses.WrappedMedia;
import awais.instagrabber.webservices.DiscoverService;
import awais.instagrabber.webservices.ServiceCallback;
public class DiscoverPostFetchService implements PostFetcher.PostFetchService {
private static final String TAG = "DiscoverPostFetchService";
private final DiscoverService discoverService;
private final DiscoverService.TopicalExploreRequest topicalExploreRequest;
private boolean moreAvailable = false;
public DiscoverPostFetchService(final DiscoverService.TopicalExploreRequest topicalExploreRequest) {
this.topicalExploreRequest = topicalExploreRequest;
discoverService = DiscoverService.getInstance();
}
@Override
public void fetch(final FetchListener> fetchListener) {
discoverService.topicalExplore(topicalExploreRequest, new ServiceCallback() {
@Override
public void onSuccess(final TopicalExploreFeedResponse result) {
if (result == null) {
onFailure(new RuntimeException("result is null"));
return;
}
moreAvailable = result.getMoreAvailable();
topicalExploreRequest.setMaxId(result.getNextMaxId());
final List items = result.getItems();
final List posts;
if (items == null) {
posts = Collections.emptyList();
} else {
posts = items.stream()
.map(WrappedMedia::getMedia)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
if (fetchListener != null) {
fetchListener.onResult(posts);
}
}
@Override
public void onFailure(final Throwable t) {
if (fetchListener != null) {
fetchListener.onFailure(t);
}
}
});
}
@Override
public void reset() {
topicalExploreRequest.setMaxId(null);
}
@Override
public boolean hasNextPage() {
return moreAvailable;
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/asyncs/FeedPostFetchService.java
================================================
package awais.instagrabber.asyncs;
import java.util.ArrayList;
import java.util.List;
import awais.instagrabber.customviews.helpers.PostFetcher;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.PostsFetchResponse;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.webservices.FeedService;
import awais.instagrabber.webservices.ServiceCallback;
import static awais.instagrabber.utils.Utils.settingsHelper;
public class FeedPostFetchService implements PostFetcher.PostFetchService {
private static final String TAG = "FeedPostFetchService";
private final FeedService feedService;
private String nextCursor;
private boolean hasNextPage;
public FeedPostFetchService() {
feedService = FeedService.getInstance();
}
@Override
public void fetch(final FetchListener> fetchListener) {
final List feedModels = new ArrayList<>();
final String cookie = settingsHelper.getString(Constants.COOKIE);
final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie);
final String deviceUuid = settingsHelper.getString(Constants.DEVICE_UUID);
feedModels.clear();
feedService.fetch(csrfToken, deviceUuid, nextCursor, new ServiceCallback() {
@Override
public void onSuccess(final PostsFetchResponse result) {
if (result == null && feedModels.size() > 0) {
fetchListener.onResult(feedModels);
return;
} else if (result == null) return;
nextCursor = result.getNextCursor();
hasNextPage = result.getHasNextPage();
final List mediaResults = result.getFeedModels();
feedModels.addAll(mediaResults);
if (fetchListener != null) {
// if (feedModels.size() < 15 && hasNextPage) {
// feedService.fetch(csrfToken, nextCursor, this);
// } else {
fetchListener.onResult(feedModels);
// }
}
}
@Override
public void onFailure(final Throwable t) {
if (fetchListener != null) {
fetchListener.onFailure(t);
}
}
});
}
@Override
public void reset() {
nextCursor = null;
}
@Override
public boolean hasNextPage() {
return hasNextPage;
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/asyncs/HashtagPostFetchService.java
================================================
package awais.instagrabber.asyncs;
import java.util.List;
import awais.instagrabber.customviews.helpers.PostFetcher;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.repositories.responses.Hashtag;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.PostsFetchResponse;
import awais.instagrabber.utils.CoroutineUtilsKt;
import awais.instagrabber.webservices.GraphQLRepository;
import awais.instagrabber.webservices.ServiceCallback;
import awais.instagrabber.webservices.TagsService;
import kotlinx.coroutines.Dispatchers;
public class HashtagPostFetchService implements PostFetcher.PostFetchService {
private final TagsService tagsService;
private final GraphQLRepository graphQLRepository;
private final Hashtag hashtagModel;
private String nextMaxId;
private boolean moreAvailable;
private final boolean isLoggedIn;
public HashtagPostFetchService(final Hashtag hashtagModel, final boolean isLoggedIn) {
this.hashtagModel = hashtagModel;
this.isLoggedIn = isLoggedIn;
tagsService = isLoggedIn ? TagsService.getInstance() : null;
graphQLRepository = isLoggedIn ? null : GraphQLRepository.Companion.getInstance();
}
@Override
public void fetch(final FetchListener> fetchListener) {
final ServiceCallback cb = new ServiceCallback() {
@Override
public void onSuccess(final PostsFetchResponse result) {
if (result == null) return;
nextMaxId = result.getNextCursor();
moreAvailable = result.getHasNextPage();
if (fetchListener != null) {
fetchListener.onResult(result.getFeedModels());
}
}
@Override
public void onFailure(final Throwable t) {
// Log.e(TAG, "onFailure: ", t);
if (fetchListener != null) {
fetchListener.onFailure(t);
}
}
};
if (isLoggedIn) tagsService.fetchPosts(hashtagModel.getName().toLowerCase(), nextMaxId, cb);
else graphQLRepository.fetchHashtagPosts(
hashtagModel.getName().toLowerCase(),
nextMaxId,
CoroutineUtilsKt.getContinuation((postsFetchResponse, throwable) -> {
if (throwable != null) {
cb.onFailure(throwable);
return;
}
cb.onSuccess(postsFetchResponse);
}, Dispatchers.getIO())
);
}
@Override
public void reset() {
nextMaxId = null;
}
@Override
public boolean hasNextPage() {
return moreAvailable;
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/asyncs/LocationPostFetchService.java
================================================
package awais.instagrabber.asyncs;
import java.util.List;
import awais.instagrabber.customviews.helpers.PostFetcher;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.repositories.responses.Location;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.PostsFetchResponse;
import awais.instagrabber.utils.CoroutineUtilsKt;
import awais.instagrabber.webservices.GraphQLRepository;
import awais.instagrabber.webservices.LocationService;
import awais.instagrabber.webservices.ServiceCallback;
import kotlinx.coroutines.Dispatchers;
public class LocationPostFetchService implements PostFetcher.PostFetchService {
private final LocationService locationService;
private final GraphQLRepository graphQLRepository;
private final Location locationModel;
private String nextMaxId;
private boolean moreAvailable;
private final boolean isLoggedIn;
public LocationPostFetchService(final Location locationModel, final boolean isLoggedIn) {
this.locationModel = locationModel;
this.isLoggedIn = isLoggedIn;
locationService = isLoggedIn ? LocationService.getInstance() : null;
graphQLRepository = isLoggedIn ? null : GraphQLRepository.Companion.getInstance();
}
@Override
public void fetch(final FetchListener> fetchListener) {
final ServiceCallback cb = new ServiceCallback() {
@Override
public void onSuccess(final PostsFetchResponse result) {
if (result == null) return;
nextMaxId = result.getNextCursor();
moreAvailable = result.getHasNextPage();
if (fetchListener != null) {
fetchListener.onResult(result.getFeedModels());
}
}
@Override
public void onFailure(final Throwable t) {
// Log.e(TAG, "onFailure: ", t);
if (fetchListener != null) {
fetchListener.onFailure(t);
}
}
};
if (isLoggedIn) locationService.fetchPosts(locationModel.getPk(), nextMaxId, cb);
else graphQLRepository.fetchLocationPosts(
locationModel.getPk(),
nextMaxId,
CoroutineUtilsKt.getContinuation((postsFetchResponse, throwable) -> {
if (throwable != null) {
cb.onFailure(throwable);
return;
}
cb.onSuccess(postsFetchResponse);
}, Dispatchers.getIO())
);
}
@Override
public void reset() {
nextMaxId = null;
}
@Override
public boolean hasNextPage() {
return moreAvailable;
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/asyncs/ProfilePostFetchService.java
================================================
package awais.instagrabber.asyncs;
import java.util.List;
import awais.instagrabber.customviews.helpers.PostFetcher;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.PostsFetchResponse;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.utils.CoroutineUtilsKt;
import awais.instagrabber.webservices.GraphQLRepository;
import awais.instagrabber.webservices.ProfileRepository;
import awais.instagrabber.webservices.ServiceCallback;
import kotlin.coroutines.Continuation;
import kotlinx.coroutines.Dispatchers;
public class ProfilePostFetchService implements PostFetcher.PostFetchService {
private static final String TAG = "ProfilePostFetchService";
private final ProfileRepository profileRepository;
private final GraphQLRepository graphQLRepository;
private final User profileModel;
private final boolean isLoggedIn;
private String nextMaxId;
private boolean moreAvailable;
public ProfilePostFetchService(final User profileModel, final boolean isLoggedIn) {
this.profileModel = profileModel;
this.isLoggedIn = isLoggedIn;
graphQLRepository = isLoggedIn ? null : GraphQLRepository.Companion.getInstance();
profileRepository = isLoggedIn ? ProfileRepository.Companion.getInstance() : null;
}
@Override
public void fetch(final FetchListener> fetchListener) {
final Continuation cb = CoroutineUtilsKt.getContinuation((result, t) -> {
if (t != null) {
if (fetchListener != null) {
fetchListener.onFailure(t);
}
return;
}
if (result == null) return;
nextMaxId = result.getNextCursor();
moreAvailable = result.getHasNextPage();
if (fetchListener != null) {
fetchListener.onResult(result.getFeedModels());
}
}, Dispatchers.getIO());
if (isLoggedIn) profileRepository.fetchPosts(profileModel.getPk(), nextMaxId, cb);
else graphQLRepository.fetchProfilePosts(
profileModel.getPk(),
30,
nextMaxId,
profileModel,
cb
);
}
@Override
public void reset() {
nextMaxId = null;
}
@Override
public boolean hasNextPage() {
return moreAvailable;
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/asyncs/SavedPostFetchService.java
================================================
package awais.instagrabber.asyncs;
import java.util.List;
import awais.instagrabber.customviews.helpers.PostFetcher;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.models.enums.PostItemType;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.PostsFetchResponse;
import awais.instagrabber.utils.CoroutineUtilsKt;
import awais.instagrabber.webservices.GraphQLRepository;
import awais.instagrabber.webservices.ProfileRepository;
import kotlin.coroutines.Continuation;
import kotlinx.coroutines.Dispatchers;
public class SavedPostFetchService implements PostFetcher.PostFetchService {
private final ProfileRepository profileRepository;
private final GraphQLRepository graphQLRepository;
private final long profileId;
private final PostItemType type;
private final boolean isLoggedIn;
private String nextMaxId;
private final String collectionId;
private boolean moreAvailable;
public SavedPostFetchService(final long profileId, final PostItemType type, final boolean isLoggedIn, final String collectionId) {
this.profileId = profileId;
this.type = type;
this.isLoggedIn = isLoggedIn;
this.collectionId = collectionId;
graphQLRepository = isLoggedIn ? null : GraphQLRepository.Companion.getInstance();
profileRepository = isLoggedIn ? ProfileRepository.Companion.getInstance() : null;
}
@Override
public void fetch(final FetchListener> fetchListener) {
final Continuation callback = CoroutineUtilsKt.getContinuation((result, t) -> {
if (t != null) {
if (fetchListener != null) {
fetchListener.onFailure(t);
}
return;
}
if (result == null) return;
nextMaxId = result.getNextCursor();
moreAvailable = result.getHasNextPage();
if (fetchListener != null) {
fetchListener.onResult(result.getFeedModels());
}
}, Dispatchers.getIO());
switch (type) {
case LIKED:
profileRepository.fetchLiked(nextMaxId, callback);
break;
case TAGGED:
if (isLoggedIn) profileRepository.fetchTagged(profileId, nextMaxId, callback);
else graphQLRepository.fetchTaggedPosts(
profileId,
30,
nextMaxId,
callback
);
break;
case COLLECTION:
case SAVED:
profileRepository.fetchSaved(nextMaxId, collectionId, callback);
break;
}
}
@Override
public void reset() {
nextMaxId = null;
}
@Override
public boolean hasNextPage() {
return moreAvailable;
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/backup/BarinstaBackupAgent.kt
================================================
package awais.instagrabber.backup
import android.app.backup.BackupAgent
import android.app.backup.BackupDataInput
import android.app.backup.BackupDataOutput
import android.app.backup.FullBackupDataOutput
import android.os.ParcelFileDescriptor
import awais.instagrabber.fragments.settings.PreferenceKeys
import awais.instagrabber.utils.Utils.settingsHelper
class BarinstaBackupAgent : BackupAgent() {
override fun onFullBackup(data: FullBackupDataOutput?) {
super.onFullBackup(if (settingsHelper.getBoolean(PreferenceKeys.PREF_AUTO_BACKUP_ENABLED)) data else null)
}
// no key-value backups
override fun onBackup(oldState: ParcelFileDescriptor?,
data: BackupDataOutput?, newState: ParcelFileDescriptor?) {}
override fun onRestore(data: BackupDataInput, appVersionCode: Int,
newState: ParcelFileDescriptor) {}
}
================================================
FILE: app/src/main/java/awais/instagrabber/broadcasts/DMRefreshBroadcastReceiver.java
================================================
package awais.instagrabber.broadcasts;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
public class DMRefreshBroadcastReceiver extends BroadcastReceiver {
public static final String ACTION_REFRESH_DM = "action_refresh_dm";
private final OnDMRefreshCallback callback;
public DMRefreshBroadcastReceiver(final OnDMRefreshCallback callback) {
this.callback = callback;
}
@Override
public void onReceive(final Context context, final Intent intent) {
if (callback == null) return;
final String action = intent.getAction();
if (action == null) return;
if (!action.equals(ACTION_REFRESH_DM)) return;
callback.onReceive();
}
public interface OnDMRefreshCallback {
void onReceive();
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/customviews/BarinstaFragmentNavigator.kt
================================================
package awais.instagrabber.customviews
import android.content.Context
import androidx.fragment.app.FragmentManager
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavOptions
import androidx.navigation.Navigator
import androidx.navigation.fragment.FragmentNavigator
import androidx.navigation.navOptions
import awais.instagrabber.R
import awais.instagrabber.fragments.settings.PreferenceKeys
import awais.instagrabber.utils.Utils
private val defaultNavOptions = navOptions {
anim {
enter = R.anim.slide_in_right
exit = R.anim.slide_out_left
popEnter = android.R.anim.slide_in_left
popExit = android.R.anim.slide_out_right
}
}
private val emptyNavOptions = navOptions {}
/**
* Needs to replace FragmentNavigator and replacing is done with name in annotation.
* Navigation method will use defaults for fragments transitions animations.
*/
@Navigator.Name("fragment")
class BarinstaFragmentNavigator(
context: Context,
fragmentManager: FragmentManager,
containerId: Int
) : FragmentNavigator(context, fragmentManager, containerId) {
override fun navigate(
entries: List,
navOptions: NavOptions?,
navigatorExtras: Navigator.Extras?
) {
val disableTransitions = Utils.settingsHelper.getBoolean(PreferenceKeys.PREF_DISABLE_SCREEN_TRANSITIONS)
if (disableTransitions) {
super.navigate(entries, navOptions, navigatorExtras)
return
}
// this will try to fill in empty animations with defaults when no shared element transitions
// https://developer.android.com/guide/navigation/navigation-animate-transitions#shared-element
val hasSharedElements = navigatorExtras != null && navigatorExtras is Extras
val navOptions1 = if (hasSharedElements) navOptions else navOptions.fillEmptyAnimationsWithDefaults()
super.navigate(entries, navOptions1, navigatorExtras)
}
private fun NavOptions?.fillEmptyAnimationsWithDefaults(): NavOptions =
this?.copyNavOptionsWithDefaultAnimations() ?: defaultNavOptions
private fun NavOptions.copyNavOptionsWithDefaultAnimations(): NavOptions = let { originalNavOptions ->
navOptions {
launchSingleTop = originalNavOptions.shouldLaunchSingleTop()
popUpTo(originalNavOptions.popUpToId) {
inclusive = originalNavOptions.isPopUpToInclusive()
saveState = originalNavOptions.shouldPopUpToSaveState()
}
originalNavOptions.popUpToRoute?.let {
popUpTo(it) {
inclusive = originalNavOptions.isPopUpToInclusive()
saveState = originalNavOptions.shouldPopUpToSaveState()
}
}
restoreState = originalNavOptions.shouldRestoreState()
anim {
enter =
if (originalNavOptions.enterAnim == emptyNavOptions.enterAnim) defaultNavOptions.enterAnim
else originalNavOptions.enterAnim
exit =
if (originalNavOptions.exitAnim == emptyNavOptions.exitAnim) defaultNavOptions.exitAnim
else originalNavOptions.exitAnim
popEnter =
if (originalNavOptions.popEnterAnim == emptyNavOptions.popEnterAnim) defaultNavOptions.popEnterAnim
else originalNavOptions.popEnterAnim
popExit =
if (originalNavOptions.popExitAnim == emptyNavOptions.popExitAnim) defaultNavOptions.popExitAnim
else originalNavOptions.popExitAnim
}
}
}
private companion object {
private const val TAG = "FragmentNavigator"
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/customviews/BarinstaNavHostFragment.kt
================================================
package awais.instagrabber.customviews
import androidx.navigation.NavHostController
import androidx.navigation.fragment.NavHostFragment
class BarinstaNavHostFragment : NavHostFragment() {
override fun onCreateNavHostController(navHostController: NavHostController) {
super.onCreateNavHostController(navHostController)
navHostController.navigatorProvider.addNavigator(
// this replaces FragmentNavigator
BarinstaFragmentNavigator(requireContext(), childFragmentManager, id)
)
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/customviews/ChatMessageLayout.java
================================================
package awais.instagrabber.customviews;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import awais.instagrabber.R;
public class ChatMessageLayout extends FrameLayout {
private FrameLayout viewPartMain;
private View viewPartInfo;
private TypedArray a;
private int viewPartInfoWidth;
private int viewPartInfoHeight;
// private boolean withGroupHeader = false;
public ChatMessageLayout(@NonNull final Context context) {
super(context);
}
public ChatMessageLayout(@NonNull final Context context, @Nullable final AttributeSet attrs) {
super(context, attrs);
a = context.obtainStyledAttributes(attrs, R.styleable.ChatMessageLayout, 0, 0);
}
public ChatMessageLayout(@NonNull final Context context, @Nullable final AttributeSet attrs, final int defStyleAttr) {
super(context, attrs, defStyleAttr);
a = context.obtainStyledAttributes(attrs, R.styleable.ChatMessageLayout, defStyleAttr, 0);
}
public ChatMessageLayout(@NonNull final Context context, @Nullable final AttributeSet attrs, final int defStyleAttr, final int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
a = context.obtainStyledAttributes(attrs, R.styleable.ChatMessageLayout, defStyleAttr, defStyleRes);
}
// public void setWithGroupHeader(boolean withGroupHeader) {
// this.withGroupHeader = withGroupHeader;
// }
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
try {
viewPartMain = findViewById(a.getResourceId(R.styleable.ChatMessageLayout_viewPartMain, -1));
viewPartInfo = findViewById(a.getResourceId(R.styleable.ChatMessageLayout_viewPartInfo, -1));
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize;
// heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (viewPartMain == null || viewPartInfo == null || widthSize <= 0) {
return;
}
final View firstChild = viewPartMain.getChildAt(0);
if (firstChild == null) return;
final int firstChildId = firstChild.getId();
int availableWidth = widthSize - getPaddingLeft() - getPaddingRight();
// int availableHeight = heightSize - getPaddingTop() - getPaddingBottom();
final LayoutParams viewPartMainLayoutParams = (LayoutParams) viewPartMain.getLayoutParams();
final int viewPartMainWidth = viewPartMain.getMeasuredWidth() + viewPartMainLayoutParams.leftMargin + viewPartMainLayoutParams.rightMargin;
final int viewPartMainHeight = viewPartMain.getMeasuredHeight() + viewPartMainLayoutParams.topMargin + viewPartMainLayoutParams.bottomMargin;
final LayoutParams viewPartInfoLayoutParams = (LayoutParams) viewPartInfo.getLayoutParams();
viewPartInfoWidth = viewPartInfo.getMeasuredWidth() + viewPartInfoLayoutParams.leftMargin + viewPartInfoLayoutParams.rightMargin;
viewPartInfoHeight = viewPartInfo.getMeasuredHeight() + viewPartInfoLayoutParams.topMargin + viewPartInfoLayoutParams.bottomMargin;
widthSize = getPaddingLeft() + getPaddingRight();
heightSize = getPaddingTop() + getPaddingBottom();
if (firstChildId == R.id.media_container) {
widthSize += viewPartMainWidth;
heightSize += viewPartMainHeight;
} else if (firstChildId == R.id.raven_media_container || firstChildId == R.id.profile_container || firstChildId == R.id.voice_media
|| firstChildId == R.id.story_container || firstChildId == R.id.media_share_container || firstChildId == R.id.link_container
|| firstChildId == R.id.ivAnimatedMessage || firstChildId == R.id.reel_share_container) {
widthSize += viewPartMainWidth;
heightSize += viewPartMainHeight + viewPartInfoHeight;
} else {
int viewPartMainLineCount = 1;
float viewPartMainLastLineWidth = 0;
final TextView textMessage;
if (firstChild instanceof TextView) {
textMessage = (TextView) firstChild;
}
else textMessage = null;
if (textMessage != null) {
viewPartMainLineCount = textMessage.getLineCount();
viewPartMainLastLineWidth = viewPartMainLineCount > 0
? textMessage.getLayout().getLineWidth(viewPartMainLineCount - 1)
: 0;
// also include start left padding
viewPartMainLastLineWidth += textMessage.getPaddingLeft();
}
final float lastLineWithInfoWidth = viewPartMainLastLineWidth + viewPartInfoWidth;
if (viewPartMainLineCount > 1 && lastLineWithInfoWidth <= viewPartMain.getMeasuredWidth()) {
widthSize += viewPartMainWidth;
heightSize += viewPartMainHeight;
} else if (viewPartMainLineCount > 1 && (lastLineWithInfoWidth > availableWidth)) {
widthSize += viewPartMainWidth;
heightSize += viewPartMainHeight + viewPartInfoHeight;
} else if (viewPartMainLineCount == 1 && (viewPartMainWidth + viewPartInfoWidth > availableWidth)) {
widthSize += viewPartMain.getMeasuredWidth();
heightSize += viewPartMainHeight + viewPartInfoHeight;
} else {
heightSize += viewPartMainHeight;
widthSize += viewPartMainWidth + viewPartInfoWidth;
}
// if (isInEditMode()) {
// TextView wDebugView = (TextView) ((ViewGroup) this.getParent()).findViewWithTag("debug");
// wDebugView.setText(lastLineWithInfoWidth
// + "\n" + availableWidth
// + "\n" + viewPartMain.getMeasuredWidth()
// + "\n" + (lastLineWithInfoWidth <= viewPartMain.getMeasuredWidth())
// + "\n" + (lastLineWithInfoWidth > availableWidth)
// + "\n" + (viewPartMainWidth + viewPartInfoWidth > availableWidth));
// }
}
setMeasuredDimension(widthSize, heightSize);
super.onMeasure(MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY));
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (viewPartMain == null || viewPartInfo == null) {
return;
}
// if (withGroupHeader) {
// viewPartMain.layout(
// getPaddingLeft(),
// getPaddingTop() - Utils.convertDpToPx(4),
// viewPartMain.getWidth() + getPaddingLeft(),
// viewPartMain.getHeight() + getPaddingTop());
//
// } else {
viewPartMain.layout(
getPaddingLeft(),
getPaddingTop(),
viewPartMain.getWidth() + getPaddingLeft(),
viewPartMain.getHeight() + getPaddingTop());
// }
viewPartInfo.layout(
right - left - viewPartInfoWidth - getPaddingRight(),
bottom - top - getPaddingBottom() - viewPartInfoHeight,
right - left - getPaddingRight(),
bottom - top - getPaddingBottom());
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/customviews/CircularImageView.java
================================================
package awais.instagrabber.customviews;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Color;
import android.util.AttributeSet;
import androidx.annotation.Nullable;
import com.facebook.drawee.drawable.ScalingUtils;
import com.facebook.drawee.generic.GenericDraweeHierarchy;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
import com.facebook.drawee.generic.GenericDraweeHierarchyInflater;
import com.facebook.drawee.generic.RoundingParams;
import com.facebook.drawee.view.SimpleDraweeView;
import awais.instagrabber.R;
public class CircularImageView extends SimpleDraweeView {
public CircularImageView(Context context, GenericDraweeHierarchy hierarchy) {
super(context);
setHierarchy(hierarchy);
}
public CircularImageView(final Context context) {
super(context);
inflateHierarchy(context, null);
}
public CircularImageView(final Context context, final AttributeSet attrs) {
super(context, attrs);
inflateHierarchy(context, attrs);
}
public CircularImageView(final Context context, final AttributeSet attrs, final int defStyleAttr) {
super(context, attrs, defStyleAttr);
inflateHierarchy(context, attrs);
}
protected void inflateHierarchy(Context context, @Nullable AttributeSet attrs) {
Resources resources = context.getResources();
final RoundingParams roundingParams = RoundingParams.asCircle();
GenericDraweeHierarchyBuilder builder = new GenericDraweeHierarchyBuilder(resources)
.setRoundingParams(roundingParams)
.setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER);
GenericDraweeHierarchyInflater.updateBuilder(builder, context, attrs);
setAspectRatio(builder.getDesiredAspectRatio());
setHierarchy(builder.build());
setBackgroundResource(R.drawable.shape_oval_light);
}
/* types: 0 clear, 1 green (feed bestie / has story), 2 red (live) */
public void setStoriesBorder(final int type) {
// private final int borderSize = 8;
final int color = type == 2 ? Color.RED : Color.GREEN;
RoundingParams roundingParams = getHierarchy().getRoundingParams();
if (roundingParams == null) {
roundingParams = RoundingParams.asCircle().setRoundingMethod(RoundingParams.RoundingMethod.BITMAP_ONLY);
}
roundingParams.setBorder(color, type == 0 ? 0f : 5.0f);
getHierarchy().setRoundingParams(roundingParams);
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/customviews/CommentMentionClickSpan.java
================================================
package awais.instagrabber.customviews;
import android.text.TextPaint;
import android.text.style.ClickableSpan;
import android.view.View;
import androidx.annotation.NonNull;
public final class CommentMentionClickSpan extends ClickableSpan {
@Override
public void onClick(@NonNull final View widget) { }
@Override
public void updateDrawState(@NonNull final TextPaint ds) {
ds.setColor(ds.linkColor);
}
}
================================================
FILE: app/src/main/java/awais/instagrabber/customviews/DirectItemContextMenu.java
================================================
package awais.instagrabber.customviews;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.Rect;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.ImageView;
import android.widget.PopupWindow;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.widget.AppCompatEditText;
import androidx.appcompat.widget.AppCompatImageView;
import androidx.appcompat.widget.AppCompatTextView;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.util.Pair;
import java.util.List;
import java.util.function.Function;
import awais.instagrabber.R;
import awais.instagrabber.animations.RoundedRectRevealOutlineProvider;
import awais.instagrabber.customviews.emoji.Emoji;
import awais.instagrabber.customviews.emoji.ReactionsManager;
import awais.instagrabber.databinding.LayoutDirectItemOptionsBinding;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import static android.view.View.MeasureSpec.makeMeasureSpec;
public class DirectItemContextMenu extends PopupWindow {
private static final String TAG = DirectItemContextMenu.class.getSimpleName();
private static final int DO_NOT_UPDATE_FLAG = -1;
private static final int DURATION = 300;
private final Context context;
private final boolean showReactions;
private final ReactionsManager reactionsManager;
private final int emojiSize;
private final int emojiMargin;
private final int emojiMarginHalf;
private final Rect startRect = new Rect();
private final Rect endRect = new Rect();
private final TimeInterpolator revealInterpolator = new AccelerateDecelerateInterpolator();
private final AnimatorListenerAdapter exitAnimationListener;
private final TypedValue selectableItemBackgroundBorderless;
private final TypedValue selectableItemBackground;
private final int dividerHeight;
private final int optionHeight;
private final int optionPadding;
private final int addAdjust;
private final boolean hasOptions;
private final List options;
private final int widthWithoutReactions;
private AnimatorSet openCloseAnimator;
private Point location;
private Point point;
private OnReactionClickListener onReactionClickListener;
private OnOptionSelectListener onOptionSelectListener;
private OnAddReactionClickListener onAddReactionListener;
public DirectItemContextMenu(@NonNull final Context context, final boolean showReactions, final List options) {
super(context);
this.context = context;
this.showReactions = showReactions;
this.options = options;
if (!showReactions && (options == null || options.isEmpty())) {
throw new IllegalArgumentException("showReactions is set false and options are empty");
}
reactionsManager = ReactionsManager.getInstance(context);
final Resources resources = context.getResources();
emojiSize = resources.getDimensionPixelSize(R.dimen.reaction_picker_emoji_size);
emojiMargin = resources.getDimensionPixelSize(R.dimen.reaction_picker_emoji_margin);
emojiMarginHalf = emojiMargin / 2;
addAdjust = resources.getDimensionPixelSize(R.dimen.reaction_picker_add_padding_adjustment);
dividerHeight = resources.getDimensionPixelSize(R.dimen.horizontal_divider_height);
optionHeight = resources.getDimensionPixelSize(R.dimen.reaction_picker_option_height);
optionPadding = resources.getDimensionPixelSize(R.dimen.dm_message_card_radius);
widthWithoutReactions = resources.getDimensionPixelSize(R.dimen.dm_item_context_min_width);
exitAnimationListener = new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(final Animator animation) {
openCloseAnimator = null;
point = null;
getContentView().post(DirectItemContextMenu.super::dismiss);
}
};
selectableItemBackgroundBorderless = new TypedValue();
context.getTheme().resolveAttribute(android.R.attr.selectableItemBackgroundBorderless, selectableItemBackgroundBorderless, true);
selectableItemBackground = new TypedValue();
context.getTheme().resolveAttribute(android.R.attr.selectableItemBackground, selectableItemBackground, true);
hasOptions = options != null && !options.isEmpty();
}
public void show(@NonNull View rootView, @NonNull final Point location) {
final View content = createContentView();
content.measure(makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
setup(content);
// rootView.getParent().requestDisallowInterceptTouchEvent(true);
// final Point correctedLocation = new Point(location.x, location.y - emojiSize * 2);
this.location = location;
showAtLocation(rootView, Gravity.TOP | Gravity.START, location.x, location.y);
// fixPopupLocation(popupWindow, correctedLocation);
animateOpen();
}
private void setup(final View content) {
setContentView(content);
setWindowLayoutMode(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
setFocusable(true);
setOutsideTouchable(true);
setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
setBackgroundDrawable(null);
}
public void setOnOptionSelectListener(final OnOptionSelectListener onOptionSelectListener) {
this.onOptionSelectListener = onOptionSelectListener;
}
public void setOnReactionClickListener(final OnReactionClickListener onReactionClickListener) {
this.onReactionClickListener = onReactionClickListener;
}
public void setOnAddReactionListener(final OnAddReactionClickListener onAddReactionListener) {
this.onAddReactionListener = onAddReactionListener;
}
private void animateOpen() {
final View contentView = getContentView();
contentView.setVisibility(View.INVISIBLE);
contentView.post(() -> {
final AnimatorSet openAnim = new AnimatorSet();
// Rectangular reveal.
final ValueAnimator revealAnim = createOpenCloseOutlineProvider().createRevealAnimator(contentView, false);
revealAnim.setDuration(DURATION);
revealAnim.setInterpolator(revealInterpolator);
ValueAnimator fadeIn = ValueAnimator.ofFloat(0, 1);
fadeIn.setDuration(DURATION);
fadeIn.setInterpolator(revealInterpolator);
fadeIn.addUpdateListener(anim -> {
float alpha = (float) anim.getAnimatedValue();
contentView.setAlpha(revealAnim.isStarted() ? alpha : 0);
});
openAnim.play(fadeIn);
openAnim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
contentView.setAlpha(1f);
openCloseAnimator = null;
}
});
openCloseAnimator = openAnim;
openAnim.playSequentially(revealAnim);
contentView.setVisibility(View.VISIBLE);
openAnim.start();
});
}
protected void animateClose() {
endRect.setEmpty();
if (openCloseAnimator != null) {
openCloseAnimator.cancel();
}
final View contentView = getContentView();
final AnimatorSet closeAnim = new AnimatorSet();
// Rectangular reveal (reversed).
final ValueAnimator revealAnim = createOpenCloseOutlineProvider().createRevealAnimator(contentView, true);
revealAnim.setDuration(DURATION);
revealAnim.setInterpolator(revealInterpolator);
closeAnim.play(revealAnim);
ValueAnimator fadeOut = ValueAnimator.ofFloat(contentView.getAlpha(), 0);
fadeOut.setDuration(DURATION);
fadeOut.setInterpolator(revealInterpolator);
fadeOut.addUpdateListener(anim -> {
float alpha = (float) anim.getAnimatedValue();
contentView.setAlpha(revealAnim.isStarted() ? alpha : contentView.getAlpha());
});
closeAnim.playTogether(fadeOut);
closeAnim.addListener(exitAnimationListener);
openCloseAnimator = closeAnim;
closeAnim.start();
}
private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider() {
final View contentView = getContentView();
final int radius = context.getResources().getDimensionPixelSize(R.dimen.dm_message_card_radius_small);
// Log.d(TAG, "createOpenCloseOutlineProvider: " + locationOnScreen(contentView) + " " + contentView.getMeasuredWidth() + " " + contentView
// .getMeasuredHeight());
if (point == null) {
point = locationOnScreen(contentView);
}
final int left = location.x - point.x;
final int top = location.y - point.y;
startRect.set(left, top, left, top);
endRect.set(0, 0, contentView.getMeasuredWidth(), contentView.getMeasuredHeight());
return new RoundedRectRevealOutlineProvider(radius, radius, startRect, endRect);
}
public void dismiss() {
animateClose();
}
private View createContentView() {
final LayoutInflater layoutInflater = LayoutInflater.from(context);
final LayoutDirectItemOptionsBinding binding = LayoutDirectItemOptionsBinding.inflate(layoutInflater, null, false);
Pair firstLastEmojiView = null;
if (showReactions) {
firstLastEmojiView = addReactions(layoutInflater, binding.container);
}
if (hasOptions) {
View divider = null;
if (showReactions) {
if (firstLastEmojiView == null) {
throw new IllegalStateException("firstLastEmojiView is null even though reactions were added");
}
// add divider if reactions were added
divider = addDivider(binding.container,
firstLastEmojiView.first.getId(),
firstLastEmojiView.first.getId(),
firstLastEmojiView.second.getId());
((ConstraintLayout.LayoutParams) firstLastEmojiView.first.getLayoutParams()).bottomToTop = divider.getId();
}
addOptions(layoutInflater, binding.container, divider);
}
return binding.getRoot();
}
private Pair addReactions(final LayoutInflater layoutInflater, final ConstraintLayout container) {
final List reactions = reactionsManager.getReactions();
AppCompatImageView prevSquareImageView = null;
View firstImageView = null;
View lastImageView = null;
for (int i = 0; i < reactions.size(); i++) {
final Emoji reaction = reactions.get(i);
final AppCompatImageView imageView = getEmojiImageView();
final ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) imageView.getLayoutParams();
if (i == 0 && !hasOptions) {
// only connect bottom to parent bottom if there are no options
layoutParams.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID;
}
if (i == 0) {
layoutParams.topToTop = ConstraintLayout.LayoutParams.PARENT_ID;
layoutParams.startToStart = ConstraintLayout.LayoutParams.PARENT_ID;
firstImageView = imageView;
layoutParams.setMargins(emojiMargin, emojiMargin, emojiMarginHalf, emojiMargin);
} else {
layoutParams.startToEnd = prevSquareImageView.getId();
final ConstraintLayout.LayoutParams prevViewLayoutParams = (ConstraintLayout.LayoutParams) prevSquareImageView.getLayoutParams();
prevViewLayoutParams.endToStart = imageView.getId();
// always connect the other image view's top and bottom to the first image view top and bottom
layoutParams.topToTop = firstImageView.getId();
layoutParams.bottomToBottom = firstImageView.getId();
layoutParams.setMargins(emojiMarginHalf, emojiMargin, emojiMarginHalf, emojiMargin);
}
imageView.setImageDrawable(reaction.getDrawable());
imageView.setOnClickListener(view -> {
if (onReactionClickListener != null) {
onReactionClickListener.onClick(reaction);
}
dismiss();
});
container.addView(imageView);
prevSquareImageView = imageView;
}
// add the + icon
if (prevSquareImageView != null) {
final AppCompatImageView imageView = getEmojiImageView();
final ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) imageView.getLayoutParams();
layoutParams.topToTop = firstImageView.getId();
layoutParams.bottomToBottom = firstImageView.getId();
layoutParams.startToEnd = prevSquareImageView.getId();
final ConstraintLayout.LayoutParams prevViewLayoutParams = (ConstraintLayout.LayoutParams) prevSquareImageView.getLayoutParams();
prevViewLayoutParams.endToStart = imageView.getId();
layoutParams.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID;
layoutParams.setMargins(emojiMarginHalf - addAdjust, emojiMargin - addAdjust, emojiMargin - addAdjust, emojiMargin - addAdjust);
imageView.setImageResource(R.drawable.ic_add);
imageView.setOnClickListener(view -> {
if (onAddReactionListener != null) {
onAddReactionListener.onAdd();
}
dismiss();
});
lastImageView = imageView;
container.addView(imageView);
}
return new Pair<>(firstImageView, lastImageView);
}
@NonNull
private AppCompatImageView getEmojiImageView() {
final AppCompatImageView imageView = new AppCompatImageView(context);
final ConstraintLayout.LayoutParams layoutParams = new ConstraintLayout.LayoutParams(emojiSize, emojiSize);
imageView.setBackgroundResource(selectableItemBackgroundBorderless.resourceId);
imageView.setScaleType(ImageView.ScaleType.FIT_CENTER);
imageView.setId(SquareImageView.generateViewId());
imageView.setLayoutParams(layoutParams);
return imageView;
}
private void addOptions(final LayoutInflater layoutInflater,
final ConstraintLayout container,
@Nullable final View divider) {
View prevOptionView = null;
if (!showReactions) {
container.getLayoutParams().width = widthWithoutReactions;
}
for (int i = 0; i < options.size(); i++) {
final MenuItem menuItem = options.get(i);
final AppCompatTextView textView = getTextView();
final ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) textView.getLayoutParams();
layoutParams.startToStart = ConstraintLayout.LayoutParams.PARENT_ID;
layoutParams.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID;
if (i == 0) {
if (divider != null) {
layoutParams.topToBottom = divider.getId();
((ConstraintLayout.LayoutParams) divider.getLayoutParams()).bottomToTop = textView.getId();
} else {
// if divider is null mean reactions were not added, so connect top to top of parent
layoutParams.topToTop = ConstraintLayout.LayoutParams.PARENT_ID;
layoutParams.topMargin = emojiMargin; // material design spec (https://material.io/components/menus#specs)
}
} else {
layoutParams.topToBottom = prevOptionView.getId();
final ConstraintLayout.LayoutParams prevLayoutParams = (ConstraintLayout.LayoutParams) prevOptionView.getLayoutParams();
prevLayoutParams.bottomToTop = textView.getId();
}
if (i == options.size() - 1) {
layoutParams.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID;
layoutParams.bottomMargin = emojiMargin; // material design spec (https://material.io/components/menus#specs)
}
textView.setText(context.getString(menuItem.getTitleRes()));
textView.setOnClickListener(v -> {
if (onOptionSelectListener != null) {
onOptionSelectListener.onSelect(menuItem.getItemId(), menuItem.getCallback());
}
dismiss();
});
container.addView(textView);
prevOptionView = textView;
}
}
private AppCompatTextView getTextView() {
final AppCompatTextView textView = new AppCompatTextView(context);
textView.setId(AppCompatEditText.generateViewId());
textView.setBackgroundResource(selectableItemBackground.resourceId);
textView.setGravity(Gravity.CENTER_VERTICAL);
textView.setPaddingRelative(optionPadding, 0, optionPadding, 0);
textView.setTextAppearance(context, R.style.TextAppearance_MaterialComponents_Body1);
final ConstraintLayout.LayoutParams layoutParams = new ConstraintLayout.LayoutParams(ConstraintLayout.LayoutParams.MATCH_CONSTRAINT,
optionHeight);
textView.setLayoutParams(layoutParams);
return textView;
}
private View addDivider(final ConstraintLayout container,
final int topViewId,
final int startViewId,
final int endViewId) {
final View dividerView = new View(context);
dividerView.setId(View.generateViewId());
dividerView.setBackgroundResource(R.drawable.pref_list_divider_material);
final ConstraintLayout.LayoutParams layoutParams = new ConstraintLayout.LayoutParams(ConstraintLayout.LayoutParams.MATCH_CONSTRAINT,
dividerHeight);
layoutParams.topToBottom = topViewId;
layoutParams.startToStart = startViewId;
layoutParams.endToEnd = endViewId;
dividerView.setLayoutParams(layoutParams);
container.addView(dividerView);
return dividerView;
}
@NonNull
private Point locationOnScreen(@NonNull final View view) {
final int[] location = new int[2];
view.getLocationOnScreen(location);
return new Point(location[0], location[1]);
}
public static class MenuItem {
@IdRes
private final int itemId;
@StringRes
private final int titleRes;
/**
* Callback function
*/
private final Function