From b6b27681f044856a90147a3629e972c48e29b8da Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Sat, 3 Apr 2021 22:17:23 +0900 Subject: [PATCH 001/320] Allow generating different apks per cpu type. Reduces apk size considerably. --- app/build.gradle | 46 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 8b9632e6..6c43d6f4 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -76,6 +76,27 @@ android { } } + splits { + // Configures multiple APKs based on ABI. + abi { + // Enables building multiple APKs per ABI. + enable true + + // 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 -> @@ -84,15 +105,32 @@ android { // def versionCode = variant.versionCode def flavor = variant.flavorName - def suffix = "${versionName}-${flavor}_${builtType}" // eg. 19.1.0-github_debug or release + 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") - // append latest commit short hash for pre-release - suffix = "${versionName}.${getGitHash()}-${flavor}" // eg. 19.1.0.b123456-github + + 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 - outputFileName = "barinsta_${suffix}.apk" + 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" } } } From 04879819c50fa2cbe2699ded5eeb4e60cd8824c2 Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Sun, 4 Apr 2021 02:29:43 +0900 Subject: [PATCH 002/320] Enable splits only if it is a release build and skip if 'noAbiSplits' is passed as project prop --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 6c43d6f4..33ab6b4d 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -80,7 +80,7 @@ android { // Configures multiple APKs based on ABI. abi { // Enables building multiple APKs per ABI. - enable true + enable !project.hasProperty("noAbiSplits") && gradle.startParameter.taskNames.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. From 23a5ae84055348f0cc4cf3ba4e066201ffbb5d70 Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Fri, 9 Apr 2021 09:06:53 -0400 Subject: [PATCH 003/320] fix picture zooming issue Co-Authored-By: Ammar Githam --- .../java/awais/instagrabber/fragments/PostViewV2Fragment.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java b/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java index 0d0c7865..58caad5a 100644 --- a/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java @@ -1022,6 +1022,8 @@ public class PostViewV2Fragment extends SharedElementTransitionDialogFragment im // binding.postImage.setOnClickListener(v -> toggleDetails()); final AnimatedZoomableController zoomableController = AnimatedZoomableController.newInstance(); zoomableController.setMaxScaleFactor(3f); + zoomableController.setGestureZoomEnabled(true); + zoomableController.setEnabled(true); binding.postImage.setZoomableController(zoomableController); binding.postImage.setTapListener(new GestureDetector.SimpleOnGestureListener() { @Override From 84d82e92d395881db0d70497f6f303159ae4e81e Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Sun, 11 Apr 2021 09:45:38 -0400 Subject: [PATCH 004/320] made play in background an option per https://redd.it/mmpeg9 --- .../fragments/PostViewV2Fragment.java | 1 + .../settings/PostPreferencesFragment.java | 16 +++++++++++++--- .../responses/directmessages/DirectItem.java | 1 - .../java/awais/instagrabber/utils/Constants.java | 1 + .../awais/instagrabber/utils/SettingsHelper.java | 3 ++- app/src/main/res/values/strings.xml | 2 ++ 6 files changed, 19 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java b/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java index 58caad5a..2232a927 100644 --- a/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java @@ -334,6 +334,7 @@ public class PostViewV2Fragment extends SharedElementTransitionDialogFragment im if (bottomSheetBehavior != null) { captionState = bottomSheetBehavior.getState(); } + if (settingsHelper.getBoolean(Constants.PLAY_IN_BACKGROUND)) return; final Media media = viewModel.getMedia(); if (media == null) return; switch (media.getMediaType()) { diff --git a/app/src/main/java/awais/instagrabber/fragments/settings/PostPreferencesFragment.java b/app/src/main/java/awais/instagrabber/fragments/settings/PostPreferencesFragment.java index 14daec7f..492f574c 100644 --- a/app/src/main/java/awais/instagrabber/fragments/settings/PostPreferencesFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/settings/PostPreferencesFragment.java @@ -17,16 +17,26 @@ public class PostPreferencesFragment extends BasePreferencesFragment { final Context context = getContext(); if (context == null) return; // generalCategory.addPreference(getAutoPlayVideosPreference(context)); + screen.addPreference(getBackgroundPlayPreference(context)); screen.addPreference(getAlwaysMuteVideosPreference(context)); screen.addPreference(getShowCaptionPreference(context)); screen.addPreference(getToggleKeywordFilterPreference(context)); screen.addPreference(getEditKeywordFilterPreference(context)); } - private Preference getAutoPlayVideosPreference(@NonNull final Context context) { +// private Preference getAutoPlayVideosPreference(@NonNull final Context context) { +// final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context); +// preference.setKey(Constants.AUTOPLAY_VIDEOS); +// preference.setTitle(R.string.post_viewer_autoplay_video); +// preference.setIconSpaceReserved(false); +// return preference; +// } + + private Preference getBackgroundPlayPreference(@NonNull final Context context) { final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context); - preference.setKey(Constants.AUTOPLAY_VIDEOS); - preference.setTitle(R.string.post_viewer_autoplay_video); + preference.setKey(Constants.PLAY_IN_BACKGROUND); + preference.setTitle(R.string.post_viewer_background_play); + preference.setTitle(R.string.post_viewer_background_play_summary); preference.setIconSpaceReserved(false); return preference; } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItem.java b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItem.java index 52ad8818..221afd79 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItem.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItem.java @@ -229,7 +229,6 @@ public class DirectItem implements Cloneable, Serializable { public LocalDateTime getLocalDateTime() { if (localDateTime == null) { localDateTime = Instant.ofEpochMilli(timestamp / 1000).atZone(ZoneId.systemDefault()).toLocalDateTime(); - ; } return localDateTime; } diff --git a/app/src/main/java/awais/instagrabber/utils/Constants.java b/app/src/main/java/awais/instagrabber/utils/Constants.java index a2a5fd13..9f79641e 100644 --- a/app/src/main/java/awais/instagrabber/utils/Constants.java +++ b/app/src/main/java/awais/instagrabber/utils/Constants.java @@ -21,6 +21,7 @@ public final class Constants { public static final String DOWNLOAD_USER_FOLDER = "download_user_folder"; public static final String TOGGLE_KEYWORD_FILTER = "toggle_keyword_filter"; public static final String DOWNLOAD_PREPEND_USER_NAME = "download_user_name"; + public static final String PLAY_IN_BACKGROUND = "play_in_background"; // deprecated: public static final String BOTTOM_TOOLBAR = "bottom_toolbar"; public static final String FOLDER_SAVE_TO = "saved_to"; public static final String AUTOPLAY_VIDEOS = "autoplay_videos"; diff --git a/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java b/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java index fc568d9e..659070a9 100755 --- a/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java +++ b/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java @@ -43,6 +43,7 @@ import static awais.instagrabber.utils.Constants.HIDE_MUTED_REELS; import static awais.instagrabber.utils.Constants.KEYWORD_FILTERS; import static awais.instagrabber.utils.Constants.MARK_AS_SEEN; import static awais.instagrabber.utils.Constants.MUTED_VIDEOS; +import static awais.instagrabber.utils.Constants.PLAY_IN_BACKGROUND; import static awais.instagrabber.utils.Constants.PREF_DARK_THEME; import static awais.instagrabber.utils.Constants.PREF_EMOJI_VARIANTS; import static awais.instagrabber.utils.Constants.PREF_HASHTAG_POSTS_LAYOUT; @@ -164,7 +165,7 @@ public final class SettingsHelper { @StringDef({DOWNLOAD_USER_FOLDER, DOWNLOAD_PREPEND_USER_NAME, FOLDER_SAVE_TO, AUTOPLAY_VIDEOS, SHOW_QUICK_ACCESS_DIALOG, MUTED_VIDEOS, SHOW_CAPTIONS, CUSTOM_DATE_TIME_FORMAT_ENABLED, MARK_AS_SEEN, DM_MARK_AS_SEEN, CHECK_ACTIVITY, CHECK_UPDATES, SWAP_DATE_TIME_FORMAT_ENABLED, PREF_ENABLE_DM_NOTIFICATIONS, PREF_ENABLE_DM_AUTO_REFRESH, - FLAG_SECURE, TOGGLE_KEYWORD_FILTER, PREF_ENABLE_SENTRY, HIDE_MUTED_REELS}) + FLAG_SECURE, TOGGLE_KEYWORD_FILTER, PREF_ENABLE_SENTRY, HIDE_MUTED_REELS, PLAY_IN_BACKGROUND}) public @interface BooleanSettings {} @StringDef({PREV_INSTALL_VERSION, BROWSER_UA_CODE, APP_UA_CODE, PREF_ENABLE_DM_AUTO_REFRESH_FREQ_NUMBER}) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9cf27407..9e67c16d 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -59,6 +59,8 @@ %s\nFollowing Autoplay videos + Continue to play videos in background + When app is not in focus Always mute videos Always show post captions Select what to download From d326d9e2d79d42c3152de88e9340d0634a7b20c0 Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Sun, 11 Apr 2021 09:54:25 -0400 Subject: [PATCH 005/320] visual optimizations for the previous commit --- .../fragments/settings/PostPreferencesFragment.java | 2 +- app/src/main/res/values/strings.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/awais/instagrabber/fragments/settings/PostPreferencesFragment.java b/app/src/main/java/awais/instagrabber/fragments/settings/PostPreferencesFragment.java index 492f574c..c79d009b 100644 --- a/app/src/main/java/awais/instagrabber/fragments/settings/PostPreferencesFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/settings/PostPreferencesFragment.java @@ -36,7 +36,7 @@ public class PostPreferencesFragment extends BasePreferencesFragment { final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context); preference.setKey(Constants.PLAY_IN_BACKGROUND); preference.setTitle(R.string.post_viewer_background_play); - preference.setTitle(R.string.post_viewer_background_play_summary); + preference.setSummary(R.string.post_viewer_background_play_summary); preference.setIconSpaceReserved(false); return preference; } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9e67c16d..9f3010d6 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -59,8 +59,8 @@ %s\nFollowing Autoplay videos - Continue to play videos in background - When app is not in focus + Continue videos in background + Do not pause videos when the app is out of focus Always mute videos Always show post captions Select what to download From d8d47befab55893545ddcf8071a88922ce306ec0 Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Sun, 11 Apr 2021 10:07:28 -0400 Subject: [PATCH 006/320] favorite tab for anons --- .../instagrabber/activities/MainActivity.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/awais/instagrabber/activities/MainActivity.java b/app/src/main/java/awais/instagrabber/activities/MainActivity.java index 8882bed9..bf36e002 100644 --- a/app/src/main/java/awais/instagrabber/activities/MainActivity.java +++ b/app/src/main/java/awais/instagrabber/activities/MainActivity.java @@ -564,6 +564,13 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage private List setupAnonBottomNav() { final int selectedItemId = binding.bottomNavView.getSelectedItemId(); + final Tab favoriteTab = new Tab(R.drawable.ic_star_24, + getString(R.string.title_favorites), + false, + "favorites_nav_graph", + R.navigation.favorites_nav_graph, + R.id.favorites_nav_graph, + R.id.favoritesFragment); final Tab profileTab = new Tab(R.drawable.ic_person_24, getString(R.string.profile), false, @@ -580,12 +587,15 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage R.id.morePreferencesFragment); final Menu menu = binding.bottomNavView.getMenu(); menu.clear(); + menu.add(0, favoriteTab.getNavigationRootId(), 0, favoriteTab.getTitle()).setIcon(favoriteTab.getIconResId()); menu.add(0, profileTab.getNavigationRootId(), 0, profileTab.getTitle()).setIcon(profileTab.getIconResId()); menu.add(0, moreTab.getNavigationRootId(), 0, moreTab.getTitle()).setIcon(moreTab.getIconResId()); - if (selectedItemId != R.id.profile_nav_graph && selectedItemId != R.id.more_nav_graph) { + if (selectedItemId != R.id.profile_nav_graph + && selectedItemId != R.id.more_nav_graph + && selectedItemId != R.id.favorites_nav_graph) { setBottomNavSelectedTab(profileTab); } - return ImmutableList.of(profileTab, moreTab); + return ImmutableList.of(favoriteTab, profileTab, moreTab); } private List setupMainBottomNav() { From 14ed5e936429cf64c94bb791cd925c746b4f7b91 Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Sun, 11 Apr 2021 10:11:40 -0400 Subject: [PATCH 007/320] release prep --- app/build.gradle | 4 ++-- fastlane/metadata/android/en-US/changelogs/62.txt | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/62.txt diff --git a/app/build.gradle b/app/build.gradle index 871f8cc5..51300156 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -20,8 +20,8 @@ android { minSdkVersion 21 targetSdkVersion 29 - versionCode 61 - versionName '19.2.0' + versionCode 62 + versionName '19.2.1' multiDexEnabled true diff --git a/fastlane/metadata/android/en-US/changelogs/62.txt b/fastlane/metadata/android/en-US/changelogs/62.txt new file mode 100644 index 00000000..f227ef0f --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/62.txt @@ -0,0 +1,3 @@ +Option to play video in background, favorites tab for anonymous users, and bug fixes. + +For details see https://github.com/austinhuang0131/barinsta/releases/tag/v19.2.1 \ No newline at end of file From 6328a66fe4e472b8963fc903e6e87cfd7b11078b Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Sun, 11 Apr 2021 11:28:49 -0400 Subject: [PATCH 008/320] New Crowdin updates (#962) * New translations strings.xml (Japanese) * New translations strings.xml (Spanish) * New translations strings.xml (Italian) * New translations strings.xml (French) * New translations strings.xml (Italian) * New translations strings.xml (Polish) * New translations strings.xml (Polish) * New translations strings.xml (Russian) * New translations strings.xml (Russian) * New translations strings.xml (Greek) * New translations strings.xml (Greek) * New translations strings.xml (Greek) * New translations strings.xml (Chinese Simplified) * New translations strings.xml (Greek) * New translations strings.xml (Greek) * New translations arrays.xml (Greek) * New translations strings.xml (Greek) * New translations strings.xml (Greek) * New translations strings.xml (Spanish) * New translations strings.xml (Russian) * New translations strings.xml (Slovak) * New translations strings.xml (Turkish) * New translations strings.xml (Chinese Simplified) * New translations strings.xml (Chinese Traditional) * New translations strings.xml (Dutch) * New translations strings.xml (Vietnamese) * New translations strings.xml (Indonesian) * New translations strings.xml (Hindi) * New translations strings.xml (Persian) * New translations strings.xml (Basque) * New translations strings.xml (Polish) * New translations strings.xml (Portuguese, Brazilian) * New translations strings.xml (Catalan) * New translations strings.xml (Greek) * New translations strings.xml (French) * New translations strings.xml (Macedonian) * New translations strings.xml (Czech) * New translations strings.xml (German) * New translations strings.xml (Italian) * New translations strings.xml (Japanese) * New translations strings.xml (Odia) * Update source file strings.xml * New translations strings.xml (Chinese Simplified) --- app/src/github/res/values-pl/strings.xml | 2 +- app/src/main/res/values-ca/strings.xml | 2 + app/src/main/res/values-cs/strings.xml | 2 + app/src/main/res/values-de/strings.xml | 2 + app/src/main/res/values-el/arrays.xml | 4 +- app/src/main/res/values-el/strings.xml | 54 +++++++++++----------- app/src/main/res/values-es/strings.xml | 16 ++++--- app/src/main/res/values-eu/strings.xml | 2 + app/src/main/res/values-fa/strings.xml | 2 + app/src/main/res/values-fr/strings.xml | 4 +- app/src/main/res/values-hi/strings.xml | 2 + app/src/main/res/values-in/strings.xml | 2 + app/src/main/res/values-it/strings.xml | 18 ++++---- app/src/main/res/values-ja/strings.xml | 4 +- app/src/main/res/values-mk/strings.xml | 2 + app/src/main/res/values-nl/strings.xml | 2 + app/src/main/res/values-or/strings.xml | 2 + app/src/main/res/values-pl/strings.xml | 16 ++++--- app/src/main/res/values-pt/strings.xml | 2 + app/src/main/res/values-ru/strings.xml | 34 +++++++------- app/src/main/res/values-sk/strings.xml | 2 + app/src/main/res/values-tr/strings.xml | 2 + app/src/main/res/values-vi/strings.xml | 2 + app/src/main/res/values-zh-rCN/strings.xml | 20 ++++---- app/src/main/res/values-zh-rTW/strings.xml | 2 + 25 files changed, 124 insertions(+), 78 deletions(-) diff --git a/app/src/github/res/values-pl/strings.xml b/app/src/github/res/values-pl/strings.xml index 481f46c5..08b8c67e 100644 --- a/app/src/github/res/values-pl/strings.xml +++ b/app/src/github/res/values-pl/strings.xml @@ -1,6 +1,6 @@ Włącz Sentry - Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io + 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 diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 93a5ef0d..454747ed 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -56,6 +56,8 @@ %s\n Seguint Reproduir vídeos automàticament + Continue videos in background + Do not pause videos when the app is out of focus Silenciar sempre els vídeos Mostrar sempre els subtítols Seleccionar el que s\'ha de descarregar diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 9d444cf3..e82b94f7 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -62,6 +62,8 @@ %s\nSleduje Videa spouštět automaticky + Continue videos in background + Do not pause videos when the app is out of focus Vždy ztlumit videa Vždy zobrazovat titulek příspěvku Vyberte, co stáhnout diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 50539525..1957b57e 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -56,6 +56,8 @@ %s\nAbonniert Videos automatisch abspielen + Continue videos in background + Do not pause videos when the app is out of focus Videos immer stummschalten Bildtext immer anzeigen Datei zum Download auswählen diff --git a/app/src/main/res/values-el/arrays.xml b/app/src/main/res/values-el/arrays.xml index 77f71e18..b9f46308 100644 --- a/app/src/main/res/values-el/arrays.xml +++ b/app/src/main/res/values-el/arrays.xml @@ -39,8 +39,8 @@ Κανένα \@ - στο - πάνω + στις + στις \| - diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 3cc70379..9597947f 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -23,16 +23,16 @@ Έλεγχος για ενημερώσεις κατά την εκκίνηση Παρεμπόδιση στιγμιοτύπων οθόνης & προεπισκόπησης εφαρμογής Λήψη δημοσιεύσεων σε φακέλους ονομάτων χρηστών - Prepend Username to Filename + Προσθέστε το όνομα χρήστη πριν από το όνομα του αρχείου Επισήμανση ιστοριών ως προβληθέντων μετά την προβολή Ο δημιουργός της ιστορίας θα γνωρίζει ότι προβλήθηκε - Hide muted stories from feed + Απόκρυψη ιστοριών που βρίσκονται σε σίγαση από τη ροή Επισήμανση μηνυμάτων ως αναγνωσμένων μετά την προβολή Τα υπόλοιπα μέλη θα γνωρίζουν ότι προβλήθηκε Ενεργοποίηση ειδοποιήσεων δραστηριότητας Ταξινόμηση ροής ιστορίων Σφάλμα κατά τη φόρτωση προφίλ! Είναι το όνομα χρήστη έγκυρο; Αν ναι, μπορεί να είστε περιορισμένος. - Σφάλμα κατά τη φόρτωση λογαριασμού! Είναι το όνομα χρήστη έγκυρο; Μήπως σας έχει μπλοκάρει; + Σφάλμα κατά τη φόρτωση λογαριασμού! Είναι το όνομα χρήστη έγκυρο; Μήπως ο χρήστης σας έχει μπλοκάρει; Σφάλμα κατά τη φόρτωση hashtag! Είναι το όνομα έγκυρο; Σφάλμα κατά την φόρτωση τοποθεσίας! Είναι η διεύθυνση έγκυρη; Σφάλμα κατά τη δημιουργία φακέλου/-ων λήψης. @@ -56,6 +56,8 @@ %s\nΑκολουθείτε Αυτόματη αναπαραγωγή των βίντεο + Continue videos in background + Do not pause videos when the app is out of focus Μόνιμη σίγαση των βίντεο Μόνιμη εμφάνιση των λεζαντών των δημοσιεύσεων Επιλογή δημοσιεύσεων για λήψη @@ -93,7 +95,7 @@ Αποσύνδεση Ανώνυμη περιήγηση στο Instagram Αφαίρεση όλων των λογαριασμών - Αυτό θα αφαιρέσει όλους τους λογαριασμούς που έχουν προστεθεί στην εφαρμογή!\nΓια να αφαιρέσετε μόνο έναν λογαριασμό, πατήστε παρατεταμένα τον λογαριασμό από τον διάλογο εναλλαγής λογαριασμών.\nΘέλετε να συνεχίσετε; + Έτσι, θα αφαιρεθούν όλοι οι λογαριασμοί που έχουν προστεθεί στην εφαρμογή!\nΓια να αφαιρέσετε μόνο έναν λογαριασμό, πατήστε τον παρατεταμένα από τον διάλογο εναλλαγής λογαριασμών.\nΘέλετε να συνεχίσετε; Μορφή ημερομηνίας Δημιουργία νέας συλλογής Επεξεργασία ονόματος συλλογής @@ -102,9 +104,9 @@ Όλα τα πολυμέσα που περιέχονται θα παραμείνουν σε άλλες συλλογές. Προσθήκη στη συλλογή... Αφαίρεση από τη συλλογή - Επισημασμένο ως \"Μου αρέσει\" + Μ\'αρέσουν Αποθηκευμένα - Σε αυτήν τη φωτογραφία + Ετικέτες Μήνυμα Μου αρέσει Δε μου αρέσει @@ -240,7 +242,7 @@ Δραστηριότητα Αρχειοθήκη ιστοριών Προτεινόμενοι χρήστες - Επιλέξτε Εικόνα + Επιλογή εικόνας Μεταφόρτωση… Έχετε: %d ακόλουθοι @@ -255,7 +257,7 @@ Προφίλ Λοιπά Μηνύματα - %d επιλέχθηκε + Επιλέχθηκαν %d Η αποσύνδεση ήταν επιτυχής! Πληροφορίες Σήμανση ως αναγνωσμένο @@ -283,7 +285,7 @@ Φωτεινό θέμα Σκούρο θέμα Κόκκοι καφέ - Ουσιώδες Σκούρο + Σκουρόχρωμο Προστέθηκε στα Αγαπημένα! Στα αγαπημένα Λογαριασμοί @@ -292,7 +294,7 @@ Άγνωστο Αφαίρεση από τα αγαπημένα! Αντίγραφα ασφαλείας & Επαναφορά - Δημιουργία αντιγράφου ασφαλείας των ρυθμίσεων της εφαρμογής, των δεδομένων σύνδεσης λογαριασμού και/ή αγαπημένα σε απλό κείμενο ή κρυπτογραφημένο αρχείο για μεταγενέστερη επαναφορά. + Δημιουργία αντιγράφου ασφαλείας των ρυθμίσεων της εφαρμογής, των δεδομένων σύνδεσης του λογαριασμού και/ή των αγαπημένων, σε ακρυπτογράφητο ή κρυπτογραφημένο αρχείο για μεταγενέστερη επαναφορά. Αν δημιουργείτε αντίγραφα ασφαλείας των δεδομένων σύνδεσης λογαριασμού, αντιμετωπίστε το αρχείο ως απόρρητο και κρατήστε το σε ασφαλές μέρος! Δημιουργία νέου αρχείου αντιγράφου ασφαλείας Επαναφορά από υπάρχον αρχείο αντιγράφου ασφαλείας @@ -327,8 +329,8 @@ 2 3 Εμφάνιση ονομάτων - Εμφάνιση άβαταρ - Μέγεθος άβαταρ + Εμφάνιση εικόνας προφίλ + Μέγεθος εικόνας προφίλ Γωνίες Εμφάνιση κενού πλέγματος Απενεργοποίηση κινουμένων σχεδίων @@ -461,22 +463,22 @@ Αφαιρέθηκε λέξη-κλειδί: %s στον κατάλογο φιλτραρίσματος Επισήμανθηκε ως αναγνωσμένο Η διαγραφή απέτυχε - Throttled by Instagram because of too many API requests. Wait for some time before retrying. - Error - This account has been logged out. - Login required! - Sentry block. - User is inactive! + Περιορίστηκατε από το Instagram λόγω υπερβολικών αιτήσεων API. Περιμένετε ορισμένη ώρα προτού προσπαθήσετε ξανά. + Σφάλμα + Αυτός ο λογαριασμός έχει αποσυνδεθεί. + Απαιτείται σύνδεση! + Φραγή Sentry. + Ο χρήστης είναι ανενεργός! Αναφορά Κατάρρευσης Barinsta Επιλέξτε μια εφαρμογή ηλ. ταχυδρομείου για αποστολή αρχείων καταγραφής κατάρρευσης - Not found! - Your IP has been rate limited by Instagram. Wait for an hour and try again. - Skip this update - You\'re already on the latest version - Screen order - Other tabs - The tab order will be reflected on next launch - If saved, all DM related features will be disabled on next launch + Δε βρέθηκε! + Η διεύθυνση IP σας έχει περιοριστεί από το Instagram. Περιμένετε για μία ώρα και μετά προσπαθήστε ξανά. + Παράλειψη της ενημέρωσης + Η εφαρμογή είναι ήδη στην τελευταία έκδοση + Σειρά της οθόνης + Λοιπές καρτέλες + Η σειρά των καρτελών θα ισχύσει από την επόμενη εκκίνηση + Εάν αποθηκευτεί, όλες οι λειτουργίες που είναι σχετικές με τα Μηνύματα, θα είναι απενεργοποιημένες στην επόμενη εκκίνηση Αντιγραφή λεζάντας Αντιγραφή απάντησης diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 188dff0c..46f07e79 100755 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -56,6 +56,8 @@ %s\nSiguiendo Autorreproducir vídeos + Continue videos in background + Do not pause videos when the app is out of focus Siempre silenciar vídeos Mostrar siempre subtítulos del post Seleccionar qué descargar @@ -461,16 +463,16 @@ Se eliminó la palabra clave: %s de la lista de filtros Marcado como visto Eliminación fallida - Throttled by Instagram because of too many API requests. Wait for some time before retrying. + Restringido por Instagram por hacer demasiadas solicitudes de API. Espera un tiempo antes de reintentar. Error - This account has been logged out. - Login required! - Sentry block. - User is inactive! + Esta cuenta ha sido desconectada. + ¡Inicio de sesión requerido! + Bloqueo de Sentry. + ¡Usuario inactivo! Informe de fallos de Barinsta Seleccione una aplicación de correo electrónico para enviar registros de errores - Not found! - Your IP has been rate limited by Instagram. Wait for an hour and try again. + ¡No encontrado! + Tu IP ha sido limitada por Instagram. Espera una hora e inténtalo de nuevo. Omitir esta actualización Ya tienes la última versión Orden de pantalla diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 1d26e03d..bf364629 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -56,6 +56,8 @@ %s\nJarraituak Erreproduzitu bideoak automatikoki + Continue videos in background + Do not pause videos when the app is out of focus Mututu bideoak beti Erakutsi argazki-oina beti Hautatu zer deskargatu diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 38d8f503..e9778af9 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -56,6 +56,8 @@ %s\nدنبال کننده ها پخش خودکار فیلم ها + Continue videos in background + Do not pause videos when the app is out of focus همیشه فیلم هارو بی صدا کن Always show post captions انتخاب کن چی دانلود کنی diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 0f61d784..cd7e6a88 100755 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -56,6 +56,8 @@ %s\nAbonnements Lecture automatique des vidéos + Continue videos in background + Do not pause videos when the app is out of focus Toujours couper le son des vidéos Toujours afficher les sous-titres de publication Sélectionnez ce que vous souhaitez télécharger @@ -461,7 +463,7 @@ Mot-clé supprimé : %s de la liste de filtres Marqué comme vu Suppression non réussie - Propulsé par Instagram à cause d\'un trop grand nombre de requêtes API. Attendez un certain temps avant de réessayer. + Limité par Instagram en raison d\'un trop grand nombre de requêtes API. Attendez un certain temps avant de réessayer. Erreur Ce compte a été déconnecté. Connexion requise! diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index a7cc2d40..4d12088d 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -56,6 +56,8 @@ %s\nFollowing वीडियो ऑटोप्ले करें + Continue videos in background + Do not pause videos when the app is out of focus सर्बदा वीडियो को शब्दहिन रखें Always show post captions डाउनलोड करने के लिए चयन करें diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 3d9aeecb..ff81bfd5 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -53,6 +53,8 @@ %s\ndiikuti Otomatis putar video + Continue videos in background + Do not pause videos when the app is out of focus Selalu bisukan video Selalu tampilkan keterangan kiriman Pilih apa yang akan diunduh diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 0aeb0239..f2eb3241 100755 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -56,6 +56,8 @@ %s\nSeguiti Riproduzione automatica video + Continue videos in background + Do not pause videos when the app is out of focus Silenzia sempre i video Mostra sempre le didascalie dei post Seleziona cosa scaricare @@ -461,16 +463,16 @@ Parola chiave rimossa: %s dalla lista filtri Segnato come visto Eliminazione non riuscita - Throttled by Instagram because of too many API requests. Wait for some time before retrying. - Error - This account has been logged out. - Login required! - Sentry block. - User is inactive! + Limitato da Instagram a causa delle troppe richieste API. Aspetta un po\' prima di riprovare. + Errore + Questo account è stato disconnesso. + Login richiesto! + Blocco sentinella. + Utente è inattivo! Rapporto sugli errori di Barinsta Selezionare un\'applicazione di posta elettronica per inviare i registri di errori - Not found! - Your IP has been rate limited by Instagram. Wait for an hour and try again. + Non trovato! + Il tuo IP è stato limitato da Instagram. Aspetta un\'ora e riprova. Salta questo aggiornamento Hai già l\'ultima versione Ordine schermata diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 6818078e..ee26cad5 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -53,6 +53,8 @@ %s\nフォロー中 動画を自動再生する + Continue videos in background + Do not pause videos when the app is out of focus 動画を常にミュートする キャプションを常に表示 ダウンロード対象を選択 @@ -299,7 +301,7 @@ 保存 キャプション Edit caption - Translate caption + 翻訳を見る ビデオプレーヤーのタイムライン Liking… いいね!に失敗しました diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index c234ef9d..6d7c8e57 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -56,6 +56,8 @@ %s\nСледбеници Autoplay на видеа + Continue videos in background + Do not pause videos when the app is out of focus Секогаш гледај видеа без звук Секогаш прикажувај наслов Одбери што сакаш да превземеш diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 1a5b56bb..b2084841 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -56,6 +56,8 @@ %s\nVolgend Video\'s automatisch afspelen + Continue videos in background + Do not pause videos when the app is out of focus Video\'s altijd dempen Always show post captions Selecteer wat je wil downloaden diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml index 2ad01552..36644df4 100644 --- a/app/src/main/res/values-or/strings.xml +++ b/app/src/main/res/values-or/strings.xml @@ -56,6 +56,8 @@ %s\nଅନୁସରଣ କରୁଛନ୍ତି ଭିଡ଼ିଓ ସ୍ୱତଃ ଚାଲୁ କର + Continue videos in background + Do not pause videos when the app is out of focus ସର୍ବଦା ଭିଡ଼ିଓକୁ ଶବ୍ଦହୀନ ରଖ Always show post captions ଡାଉନଲୋଡ଼ କରିବା ପାଇଁ ଚୟନ କରନ୍ତୁ diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index b3962453..9fd78fc2 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -23,7 +23,7 @@ Sprawdź aktualizacje przy starcie Blokuj zrzuty ekranu & podgląd aplikacji Pobierz posty do folderów o nazwie użytkownika - Prepend Username to Filename + Dodaj nazwę użytkownika do nazwy pliku Oznacz relacje jako widoczne po wyświetleniu Autor relacji będzie widział, że to wyświetliłeś Ukryj wyciszone relacje z kanału @@ -62,6 +62,8 @@ %s\nobserwowanych Automatyczne odtwarzanie filmów + Continue videos in background + Do not pause videos when the app is out of focus Zawsze wyciszaj filmy Zawsze pokazuj napisy postów Wybierz, co chcesz pobrać @@ -478,15 +480,15 @@ Oznacz jako przeczytane Usuwanie nie powiodło się Throttled by Instagram because of too many API requests. Wait for some time before retrying. - Error - This account has been logged out. - Login required! + Błąd + To konto zostało wylogowane. + Wymagane logowanie! Sentry block. - User is inactive! + Użytkownik jest nieaktywny! Raport awarii Barinsta Wybierz aplikację e-mail do wysyłania dzienników awarii - Not found! - Your IP has been rate limited by Instagram. Wait for an hour and try again. + Nie znaleziono! + Twój adres IP został ograniczony przez Instagram. Poczekaj godzinę i spróbuj ponownie. Pomiń tę aktualizację Posiadasz aktualną wersję Kolejność na ekranie diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 57164840..f58f643a 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -56,6 +56,8 @@ %s\nSeguindo Reprodução automática de vídeos + Continue videos in background + Do not pause videos when the app is out of focus Sempre silenciar vídeos Sempre mostrar as legendas das publicações Selecionar o que baixar diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 6f76baed..61deddbb 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -23,10 +23,10 @@ Проверять наличие обновлений при запуске Блокировать скриншоты & превью приложения Скачать публикации в папки с именем пользователя - Prepend Username to Filename + Добавить имя пользователя к имени файла Отметить истории как увиденные после просмотра Автор истории узнает, что вы просмотрели её - Hide muted stories from feed + Скрыть заглушённые истории из ленты новостей Отметить ЛС как увиденные после просмотра Другие участники узнают, что вы просмотрели его Включить уведомления об активности @@ -62,6 +62,8 @@ %s\nПоследователей Автовоспроизведение видео + Continue videos in background + Do not pause videos when the app is out of focus Всегда заглушать видео Всегда отображать подписи к постам Выберите, что скачивать @@ -477,22 +479,22 @@ Удалено ключевое слово: %s из списка фильтров Отмечено как просмотренное Не удалось удалить - Throttled by Instagram because of too many API requests. Wait for some time before retrying. - Error - This account has been logged out. - Login required! - Sentry block. - User is inactive! + Замято Instagram\'ом из-за слишком большого количества запросов API. Подождите некоторое время перед повторной попыткой. + Ошибка + Эта учётная запись вышла из системы. + Требуется вход в систему! + Блокировка \"часового\". + Пользователь неактивен! Barinsta Crash Report Выберите приложение для отправки логов ошибки - Not found! - Your IP has been rate limited by Instagram. Wait for an hour and try again. - Skip this update - You\'re already on the latest version - Screen order - Other tabs - The tab order will be reflected on next launch - If saved, all DM related features will be disabled on next launch + Не найдено! + Ваш IP-адрес был ограничен Instagram. Подождите час и повторите попытку. + Пропустить это обновление + Вы уже используете последнюю версию + Порядок экрана + Другие вкладки + Порядок вкладок будет отражён при следующем запуске + При сохранении, все функции, связанные с ЛС, будут отключены при следующем запуске Копировать подпись Копировать ответ diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 3b91ad3a..f3e3ecc6 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -62,6 +62,8 @@ %s\nSleduje Automaticky prehrávať videá + Continue videos in background + Do not pause videos when the app is out of focus Vždy stíšiť videá Vždy zobraziť popis príspevku Vybrať čo stiahnuť diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 02bbdef6..3094c6f6 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -56,6 +56,8 @@ %s\nTakip Videoları otomatik oynat + Continue videos in background + Do not pause videos when the app is out of focus Videoları her zaman sustur Gönderi başlıklarını her zaman göster İndirmek için seçin diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index c8d69477..c35577d3 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -53,6 +53,8 @@ %s\nĐang theo dõi Tự động phát video + Continue videos in background + Do not pause videos when the app is out of focus Luôn luôn tắt âm thanh video Luôn hiển thị tiêu đề của bài viết Chọn mục tải xuống diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index e7987f20..8218ac05 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -53,9 +53,11 @@ %s\n已关注 自动播放视频 + 在后台继续播放视频 + 隐藏应用画面(锁屏、切换应用…)时不暂停视频 视频默认静音 总是显示帖子标题 - 选择要下载的 + 选择要下载的内容 当前照片 整个图集 显示快拍 @@ -453,16 +455,16 @@ 已移除关键词: %s 至过滤列表 已标记为已读 删除失败 - Throttled by Instagram because of too many API requests. Wait for some time before retrying. - Error - This account has been logged out. - Login required! - Sentry block. - User is inactive! + 您发送的API请求过多。请等待一段时间后重试。 + 错误 + 您已经退出。 + 需要登录! + Sentry 故障。 + 账户已冻结! Barinsta 崩溃报告 选择一个电子邮件应用来发送崩溃日志 - Not found! - Your IP has been rate limited by Instagram. Wait for an hour and try again. + 未找到所要的内容! + 您的IP地址已被 Instagram 设限。请等待一个小时后重试。 跳过本次更新 您现在安装的是最新版本 页面顺序 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index e7a4ea57..e5774194 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -53,6 +53,8 @@ %s\n追蹤中 自動播放影片 + Continue videos in background + Do not pause videos when the app is out of focus 永遠自動靜音影片 Always show post captions 選擇要下載的內容 From c859669ac1c585f6e40f49010bbb97f4b3fef5b7 Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Wed, 14 Apr 2021 00:17:23 +0900 Subject: [PATCH 009/320] Add search fragment with recent searches --- app/build.gradle | 20 + .../awais.instagrabber.db.AppDatabase/6.json | 227 +++++++++++ .../awais/instagrabber/db/MigrationTest.java | 51 +++ .../db/dao/RecentSearchDaoTest.java | 82 ++++ .../instagrabber/activities/MainActivity.java | 310 +++++---------- .../adapters/FavoritesAdapter.java | 4 +- .../adapters/SearchCategoryAdapter.java | 33 ++ .../adapters/SearchItemsAdapter.java | 215 +++++++++++ .../adapters/SuggestionsAdapter.java | 77 ---- .../viewholder/FavoriteViewHolder.java | 20 +- .../viewholder/SearchItemViewHolder.java | 80 ++++ .../awais/instagrabber/db/AppDatabase.java | 25 +- .../instagrabber/db/dao/RecentSearchDao.java | 37 ++ .../datasources/RecentSearchDataSource.java | 57 +++ .../db/entities/RecentSearch.java | 185 +++++++++ .../repositories/RecentSearchRepository.java | 124 ++++++ .../fragments/FavoritesFragment.java | 4 +- .../search/SearchCategoryFragment.java | 195 ++++++++++ .../fragments/search/SearchFragment.java | 245 ++++++++++++ .../models/enums/FavoriteType.java | 3 +- .../repositories/responses/Hashtag.java | 18 + .../repositories/responses/Place.java | 19 + .../responses/search/SearchItem.java | 236 ++++++++++++ .../viewmodels/AppStateViewModel.java | 9 +- .../viewmodels/SearchFragmentViewModel.java | 352 ++++++++++++++++++ app/src/main/res/layout/activity_main.xml | 24 +- .../main/res/layout/fragment_favorites.xml | 2 +- app/src/main/res/layout/fragment_search.xml | 18 + ..._suggestion.xml => item_search_result.xml} | 41 +- app/src/main/res/menu/main_menu.xml | 19 +- .../navigation/direct_messages_nav_graph.xml | 10 + .../res/navigation/discover_nav_graph.xml | 9 + .../res/navigation/favorites_nav_graph.xml | 13 +- .../main/res/navigation/feed_nav_graph.xml | 10 +- .../main/res/navigation/hashtag_nav_graph.xml | 9 + .../res/navigation/location_nav_graph.xml | 9 + .../main/res/navigation/profile_nav_graph.xml | 37 +- app/src/main/res/values/strings.xml | 3 + app/src/main/res/values/styles.xml | 9 + 39 files changed, 2455 insertions(+), 386 deletions(-) create mode 100644 app/schemas/awais.instagrabber.db.AppDatabase/6.json create mode 100644 app/src/androidTest/java/awais/instagrabber/db/MigrationTest.java create mode 100644 app/src/androidTest/java/awais/instagrabber/db/dao/RecentSearchDaoTest.java create mode 100644 app/src/main/java/awais/instagrabber/adapters/SearchCategoryAdapter.java create mode 100644 app/src/main/java/awais/instagrabber/adapters/SearchItemsAdapter.java delete mode 100755 app/src/main/java/awais/instagrabber/adapters/SuggestionsAdapter.java create mode 100644 app/src/main/java/awais/instagrabber/adapters/viewholder/SearchItemViewHolder.java create mode 100644 app/src/main/java/awais/instagrabber/db/dao/RecentSearchDao.java create mode 100644 app/src/main/java/awais/instagrabber/db/datasources/RecentSearchDataSource.java create mode 100644 app/src/main/java/awais/instagrabber/db/entities/RecentSearch.java create mode 100644 app/src/main/java/awais/instagrabber/db/repositories/RecentSearchRepository.java create mode 100644 app/src/main/java/awais/instagrabber/fragments/search/SearchCategoryFragment.java create mode 100644 app/src/main/java/awais/instagrabber/fragments/search/SearchFragment.java create mode 100644 app/src/main/java/awais/instagrabber/viewmodels/SearchFragmentViewModel.java create mode 100644 app/src/main/res/layout/fragment_search.xml rename app/src/main/res/layout/{item_suggestion.xml => item_search_result.xml} (63%) mode change 100755 => 100644 diff --git a/app/build.gradle b/app/build.gradle index 871f8cc5..044c8f41 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -33,6 +33,12 @@ android { arguments = ["room.schemaLocation": "$projectDir/schemas".toString()] } } + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + sourceSets { + androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) } compileOptions { @@ -95,6 +101,13 @@ android { outputFileName = "barinsta_${suffix}.apk" } } + + packagingOptions { + // Exclude file to avoid + // Error: Duplicate files during packaging of APK + exclude 'META-INF/LICENSE.md' + exclude 'META-INF/LICENSE-notice.md' + } } configurations.all { @@ -165,4 +178,11 @@ dependencies { githubImplementation 'io.sentry:sentry-android:4.3.0' testImplementation 'org.junit.jupiter:junit-jupiter:5.7.1' + + androidTestImplementation 'org.junit.jupiter:junit-jupiter:5.7.1' + androidTestImplementation 'androidx.test:core:1.3.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.2.6" + } diff --git a/app/schemas/awais.instagrabber.db.AppDatabase/6.json b/app/schemas/awais.instagrabber.db.AppDatabase/6.json new file mode 100644 index 00000000..4a2f5199 --- /dev/null +++ b/app/schemas/awais.instagrabber.db.AppDatabase/6.json @@ -0,0 +1,227 @@ +{ + "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')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/awais/instagrabber/db/MigrationTest.java b/app/src/androidTest/java/awais/instagrabber/db/MigrationTest.java new file mode 100644 index 00000000..5156c5ff --- /dev/null +++ b/app/src/androidTest/java/awais/instagrabber/db/MigrationTest.java @@ -0,0 +1,51 @@ +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(); + assert canonicalName != null; + 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(); + } +} diff --git a/app/src/androidTest/java/awais/instagrabber/db/dao/RecentSearchDaoTest.java b/app/src/androidTest/java/awais/instagrabber/db/dao/RecentSearchDaoTest.java new file mode 100644 index 00000000..c8a48775 --- /dev/null +++ b/app/src/androidTest/java/awais/instagrabber/db/dao/RecentSearchDaoTest.java @@ -0,0 +1,82 @@ +package awais.instagrabber.db.dao; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.room.Room; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.runner.AndroidJUnit4; + +import com.google.common.collect.ImmutableList; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.runner.RunWith; + +import java.time.LocalDateTime; +import java.util.List; + +import awais.instagrabber.db.AppDatabase; +import awais.instagrabber.db.entities.RecentSearch; +import awais.instagrabber.models.enums.FavoriteType; + +@RunWith(AndroidJUnit4.class) +public class RecentSearchDaoTest { + private static final String TAG = RecentSearchDaoTest.class.getSimpleName(); + + private RecentSearchDao dao; + private AppDatabase db; + + @Before + public void createDb() { + final Context context = ApplicationProvider.getApplicationContext(); + db = Room.inMemoryDatabaseBuilder(context, AppDatabase.class).build(); + dao = db.recentSearchDao(); + } + + @After + public void closeDb() { + db.close(); + } + + @Test + public void writeQueryDelete() { + final RecentSearch recentSearch = insertRecentSearch("1", "test1", FavoriteType.HASHTAG); + final RecentSearch byIgIdAndType = dao.getRecentSearchByIgIdAndType("1", FavoriteType.HASHTAG); + Assertions.assertEquals(recentSearch, byIgIdAndType); + dao.deleteRecentSearch(byIgIdAndType); + final RecentSearch deleted = dao.getRecentSearchByIgIdAndType("1", FavoriteType.HASHTAG); + Assertions.assertNull(deleted); + } + + @Test + public void queryAllOrdered() { + final List insertListReversed = ImmutableList + .builder() + .add(insertRecentSearch("1", "test1", FavoriteType.HASHTAG)) + .add(insertRecentSearch("2", "test2", FavoriteType.LOCATION)) + .add(insertRecentSearch("3", "test3", FavoriteType.USER)) + .add(insertRecentSearch("4", "test4", FavoriteType.USER)) + .add(insertRecentSearch("5", "test5", FavoriteType.USER)) + .build() + .reverse(); // important + final List fromDb = dao.getAllRecentSearches(); + Assertions.assertIterableEquals(insertListReversed, fromDb); + } + + @NonNull + private RecentSearch insertRecentSearch(final String igId, final String name, final FavoriteType type) { + final RecentSearch recentSearch = new RecentSearch( + igId, + name, + null, + null, + type, + LocalDateTime.now() + ); + dao.insertRecentSearch(recentSearch); + return recentSearch; + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/activities/MainActivity.java b/app/src/main/java/awais/instagrabber/activities/MainActivity.java index 8882bed9..cc833e97 100644 --- a/app/src/main/java/awais/instagrabber/activities/MainActivity.java +++ b/app/src/main/java/awais/instagrabber/activities/MainActivity.java @@ -8,19 +8,18 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; -import android.database.MatrixCursor; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; -import android.provider.BaseColumns; +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 android.widget.AutoCompleteTextView; +import android.widget.EditText; import android.widget.Toast; import androidx.annotation.IdRes; @@ -28,7 +27,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.widget.SearchView; import androidx.appcompat.widget.Toolbar; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.app.NotificationManagerCompat; @@ -50,10 +48,10 @@ import com.google.android.material.appbar.CollapsingToolbarLayout; import com.google.android.material.badge.BadgeDrawable; import com.google.android.material.behavior.HideBottomViewOnScrollBehavior; import com.google.android.material.bottomnavigation.BottomNavigationView; +import com.google.android.material.textfield.TextInputLayout; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterators; -import java.util.ArrayList; import java.util.Collections; import java.util.Deque; import java.util.List; @@ -62,9 +60,9 @@ import java.util.stream.Collectors; import awais.instagrabber.BuildConfig; import awais.instagrabber.R; -import awais.instagrabber.adapters.SuggestionsAdapter; import awais.instagrabber.asyncs.PostFetcher; import awais.instagrabber.customviews.emoji.EmojiVariantManager; +import awais.instagrabber.customviews.helpers.TextWatcherAdapter; import awais.instagrabber.databinding.ActivityMainBinding; import awais.instagrabber.fragments.PostViewV2Fragment; import awais.instagrabber.fragments.directmessages.DirectMessageInboxFragmentDirections; @@ -72,9 +70,6 @@ import awais.instagrabber.fragments.main.FeedFragment; import awais.instagrabber.fragments.settings.PreferenceKeys; import awais.instagrabber.models.IntentModel; import awais.instagrabber.models.Tab; -import awais.instagrabber.models.enums.SuggestionType; -import awais.instagrabber.repositories.responses.search.SearchItem; -import awais.instagrabber.repositories.responses.search.SearchResponse; import awais.instagrabber.services.ActivityCheckerService; import awais.instagrabber.services.DMSyncAlarmReceiver; import awais.instagrabber.utils.AppExecutors; @@ -88,10 +83,6 @@ import awais.instagrabber.utils.emoji.EmojiParser; import awais.instagrabber.viewmodels.AppStateViewModel; import awais.instagrabber.viewmodels.DirectInboxViewModel; import awais.instagrabber.webservices.RetrofitFactory; -import awais.instagrabber.webservices.SearchService; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; import static awais.instagrabber.utils.NavigationExtensions.setupWithNavController; import static awais.instagrabber.utils.Utils.settingsHelper; @@ -100,16 +91,19 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage private static final String TAG = "MainActivity"; private static final String FIRST_FRAGMENT_GRAPH_INDEX_KEY = "firstFragmentGraphIndex"; private static final String LAST_SELECT_NAV_MENU_ID = "lastSelectedNavMenuId"; + private static final List SEARCH_VISIBLE_DESTINATIONS = ImmutableList.of( + R.id.feedFragment, + R.id.profileFragment, + R.id.directMessagesInboxFragment, + R.id.discoverFragment, + R.id.favoritesFragment, + R.id.hashTagFragment, + R.id.locationFragment + ); private ActivityMainBinding binding; private LiveData currentNavControllerLiveData; private MenuItem searchMenuItem; - private SuggestionsAdapter suggestionAdapter; - private AutoCompleteTextView searchAutoComplete; - private SearchView searchView; - private SearchService searchService; - private boolean showSearch = true; - private Handler suggestionsFetchHandler; private int firstFragmentGraphIndex; private int lastSelectedNavMenuId; private boolean isActivityCheckerServiceBound = false; @@ -155,7 +149,6 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage if (savedInstanceState == null) { setupBottomNavigationBar(true); } - setupSuggestions(); if (!BuildConfig.isPre) { final boolean checkUpdates = settingsHelper.getBoolean(Constants.CHECK_UPDATES); if (checkUpdates) FlavorTown.updateCheck(this); @@ -174,9 +167,9 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage EmojiVariantManager.getInstance(); }); initEmojiCompat(); - searchService = SearchService.getInstance(); // initDmService(); initDmUnreadCount(); + initSearchInput(); } private void setupCookie() { @@ -216,25 +209,74 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage }); } + private void initSearchInput() { + binding.searchInputLayout.setEndIconOnClickListener(v -> { + final EditText editText = binding.searchInputLayout.getEditText(); + if (editText == null) return; + editText.setText(""); + }); + binding.searchInputLayout.addOnEditTextAttachedListener(textInputLayout -> { + textInputLayout.setEndIconVisible(false); + final EditText editText = textInputLayout.getEditText(); + if (editText == null) return; + editText.addTextChangedListener(new TextWatcherAdapter() { + @Override + public void afterTextChanged(final Editable s) { + binding.searchInputLayout.setEndIconVisible(!TextUtils.isEmpty(s)); + } + }); + }); + } + @Override public boolean onCreateOptionsMenu(final Menu menu) { getMenuInflater().inflate(R.menu.main_menu, menu); searchMenuItem = menu.findItem(R.id.search); - if (showSearch && currentNavControllerLiveData != null) { - final NavController navController = currentNavControllerLiveData.getValue(); - if (navController != null) { - final NavDestination currentDestination = navController.getCurrentDestination(); - if (currentDestination != null) { - final int destinationId = currentDestination.getId(); - showSearch = destinationId == R.id.profileFragment; - } + final NavController navController = currentNavControllerLiveData.getValue(); + if (navController != null) { + final NavDestination currentDestination = navController.getCurrentDestination(); + if (currentDestination != null) { + @SuppressLint("RestrictedApi") final Deque backStack = navController.getBackStack(); + setupMenu(backStack.size(), currentDestination.getId()); } } - if (!showSearch) { - searchMenuItem.setVisible(false); - return true; + // if (binding.searchInputLayout.getVisibility() == View.VISIBLE) { + // searchMenuItem.setVisible(false).setEnabled(false); + // return true; + // } + // searchMenuItem.setVisible(true).setEnabled(true); + // if (showSearch && currentNavControllerLiveData != null) { + // final NavController navController = currentNavControllerLiveData.getValue(); + // if (navController != null) { + // final NavDestination currentDestination = navController.getCurrentDestination(); + // if (currentDestination != null) { + // final int destinationId = currentDestination.getId(); + // showSearch = destinationId == R.id.profileFragment; + // } + // } + // } + // if (!showSearch) { + // searchMenuItem.setVisible(false); + // return true; + // } + // return setupSearchView(); + return true; + } + + @Override + public boolean onOptionsItemSelected(@NonNull final MenuItem item) { + if (item.getItemId() == R.id.search) { + final NavController navController = currentNavControllerLiveData.getValue(); + if (navController == null) return false; + try { + navController.navigate(R.id.action_global_search); + return true; + } catch (Exception e) { + Log.e(TAG, "onOptionsItemSelected: ", e); + } + return false; } - return setupSearchView(); + return super.onOptionsItemSelected(item); } @Override @@ -336,176 +378,6 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage notificationManager.createNotificationChannel(silentNotificationChannel); } - private void setupSuggestions() { - suggestionsFetchHandler = new Handler(); - suggestionAdapter = new SuggestionsAdapter(this, (type, query) -> { - if (searchMenuItem != null) searchMenuItem.collapseActionView(); - if (searchView != null && !searchView.isIconified()) searchView.setIconified(true); - if (currentNavControllerLiveData == null) return; - final NavController navController = currentNavControllerLiveData.getValue(); - if (navController == null) return; - final Bundle bundle = new Bundle(); - switch (type) { - case TYPE_LOCATION: - bundle.putLong("locationId", Long.parseLong(query)); - navController.navigate(R.id.action_global_locationFragment, bundle); - break; - case TYPE_HASHTAG: - bundle.putString("hashtag", query); - navController.navigate(R.id.action_global_hashTagFragment, bundle); - break; - case TYPE_USER: - bundle.putString("username", query); - navController.navigate(R.id.action_global_profileFragment, bundle); - break; - } - }); - } - - private boolean setupSearchView() { - final View actionView = searchMenuItem.getActionView(); - if (!(actionView instanceof SearchView)) return false; - searchView = (SearchView) actionView; - searchView.setSuggestionsAdapter(suggestionAdapter); - searchView.setMaxWidth(Integer.MAX_VALUE); - final View searchText = searchView.findViewById(R.id.search_src_text); - if (searchText instanceof AutoCompleteTextView) { - searchAutoComplete = (AutoCompleteTextView) searchText; - } - searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { - private boolean searchUser; - private boolean searchHash; - private Call prevSuggestionAsync; - private final String[] COLUMNS = { - BaseColumns._ID, - Constants.EXTRAS_USERNAME, - Constants.EXTRAS_NAME, - Constants.EXTRAS_TYPE, - "query", - "pfp", - "verified" - }; - private String currentSearchQuery; - - private final Callback cb = new Callback() { - @Override - public void onResponse(@NonNull final Call call, - @NonNull final Response response) { - final MatrixCursor cursor; - final SearchResponse body = response.body(); - if (body == null) { - cursor = null; - return; - } - final List result = new ArrayList<>(); - if (isLoggedIn) { - if (body.getList() != null) { - result.addAll(searchHash ? body.getList() - .stream() - .filter(i -> i.getUser() == null) - .collect(Collectors.toList()) - : body.getList()); - } - } else { - if (body.getUsers() != null && !searchHash) result.addAll(body.getUsers()); - if (body.getHashtags() != null) result.addAll(body.getHashtags()); - if (body.getPlaces() != null) result.addAll(body.getPlaces()); - } - cursor = new MatrixCursor(COLUMNS, 0); - for (int i = 0; i < result.size(); i++) { - final SearchItem suggestionModel = result.get(i); - if (suggestionModel != null) { - Object[] objects = null; - if (suggestionModel.getUser() != null) - objects = new Object[]{ - suggestionModel.getPosition(), - suggestionModel.getUser().getUsername(), - suggestionModel.getUser().getFullName(), - SuggestionType.TYPE_USER, - suggestionModel.getUser().getUsername(), - suggestionModel.getUser().getProfilePicUrl(), - suggestionModel.getUser().isVerified()}; - else if (suggestionModel.getHashtag() != null) - objects = new Object[]{ - suggestionModel.getPosition(), - suggestionModel.getHashtag().getName(), - suggestionModel.getHashtag().getSubtitle(), - SuggestionType.TYPE_HASHTAG, - suggestionModel.getHashtag().getName(), - "res:/" + R.drawable.ic_hashtag, - false}; - else if (suggestionModel.getPlace() != null) - objects = new Object[]{ - suggestionModel.getPosition(), - suggestionModel.getPlace().getTitle(), - suggestionModel.getPlace().getSubtitle(), - SuggestionType.TYPE_LOCATION, - suggestionModel.getPlace().getLocation().getPk(), - "res:/" + R.drawable.ic_location, - false}; - cursor.addRow(objects); - } - } - suggestionAdapter.changeCursor(cursor); - } - - @Override - public void onFailure(@NonNull final Call call, - @NonNull Throwable t) { - if (!call.isCanceled()) { - Log.e(TAG, "Exception on search:", t); - } - } - }; - - private final Runnable runnable = () -> { - cancelSuggestionsAsync(); - if (TextUtils.isEmpty(currentSearchQuery)) { - suggestionAdapter.changeCursor(null); - return; - } - searchUser = currentSearchQuery.charAt(0) == '@'; - searchHash = currentSearchQuery.charAt(0) == '#'; - if (currentSearchQuery.length() == 1 && (searchHash || searchUser)) { - if (searchAutoComplete != null) { - searchAutoComplete.setThreshold(2); - } - } else { - if (searchAutoComplete != null) { - searchAutoComplete.setThreshold(1); - } - prevSuggestionAsync = searchService.search(isLoggedIn, - searchUser || searchHash ? currentSearchQuery.substring(1) - : currentSearchQuery, - searchUser ? "user" : (searchHash ? "hashtag" : "blended")); - suggestionAdapter.changeCursor(null); - prevSuggestionAsync.enqueue(cb); - } - }; - - private void cancelSuggestionsAsync() { - if (prevSuggestionAsync != null) - try { - prevSuggestionAsync.cancel(); - } catch (final Exception ignored) {} - } - - @Override - public boolean onQueryTextSubmit(final String query) { - return onQueryTextChange(query); - } - - @Override - public boolean onQueryTextChange(final String query) { - suggestionsFetchHandler.removeCallbacks(runnable); - currentSearchQuery = query; - suggestionsFetchHandler.postDelayed(runnable, 800); - return true; - } - }); - return true; - } - private void setupBottomNavigationBar(final boolean setDefaultTabFromSettings) { currentTabs = !isLoggedIn ? setupAnonBottomNav() : setupMainBottomNav(); final List mainNavList = currentTabs.stream() @@ -606,20 +478,6 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage binding.bottomNavView.setSelectedItemId(navGraphRootId); } - // @NonNull - // private List getMainNavList(final int main_nav_ids) { - // final TypedArray navIds = getResources().obtainTypedArray(main_nav_ids); - // final List mainNavList = new ArrayList<>(navIds.length()); - // final int length = navIds.length(); - // for (int i = 0; i < length; i++) { - // final int resourceId = navIds.getResourceId(i, -1); - // if (resourceId < 0) continue; - // mainNavList.add(resourceId); - // } - // navIds.recycle(); - // return mainNavList; - // } - private void setupNavigation(final Toolbar toolbar, final NavController navController) { if (navController == null) return; NavigationUI.setupWithNavController(toolbar, navController); @@ -651,12 +509,10 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage private void setupMenu(final int backStackSize, final int destinationId) { if (searchMenuItem == null) return; - if (backStackSize >= 2 && destinationId == R.id.profileFragment) { - showSearch = true; + if (backStackSize >= 2 && SEARCH_VISIBLE_DESTINATIONS.contains(destinationId)) { searchMenuItem.setVisible(true); return; } - showSearch = false; searchMenuItem.setVisible(false); } @@ -935,10 +791,6 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage return currentTabs; } - // public boolean isNavRootInCurrentTabs(@IdRes final int navRootId) { - // return showBottomViewDestinations.stream().anyMatch(id -> id == navRootId); - // } - private void setNavBarDMUnreadCountBadge(final int unseenCount) { final BadgeDrawable badge = binding.bottomNavView.getOrCreateBadge(R.id.direct_messages_nav_graph); if (badge == null) return; @@ -953,4 +805,14 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage badge.setNumber(unseenCount); badge.setVisible(true); } + + @NonNull + public TextInputLayout showSearchView() { + binding.searchInputLayout.setVisibility(View.VISIBLE); + return binding.searchInputLayout; + } + + public void hideSearchView() { + binding.searchInputLayout.setVisibility(View.GONE); + } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/adapters/FavoritesAdapter.java b/app/src/main/java/awais/instagrabber/adapters/FavoritesAdapter.java index d697343a..b9d89ce7 100644 --- a/app/src/main/java/awais/instagrabber/adapters/FavoritesAdapter.java +++ b/app/src/main/java/awais/instagrabber/adapters/FavoritesAdapter.java @@ -19,7 +19,7 @@ import java.util.List; import awais.instagrabber.R; import awais.instagrabber.adapters.viewholder.FavoriteViewHolder; import awais.instagrabber.databinding.ItemFavSectionHeaderBinding; -import awais.instagrabber.databinding.ItemSuggestionBinding; +import awais.instagrabber.databinding.ItemSearchResultBinding; import awais.instagrabber.db.entities.Favorite; import awais.instagrabber.models.enums.FavoriteType; @@ -73,7 +73,7 @@ public class FavoritesAdapter extends RecyclerView.Adapter 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(); + } +} diff --git a/app/src/main/java/awais/instagrabber/adapters/SearchItemsAdapter.java b/app/src/main/java/awais/instagrabber/adapters/SearchItemsAdapter.java new file mode 100644 index 00000000..e26059de --- /dev/null +++ b/app/src/main/java/awais/instagrabber/adapters/SearchItemsAdapter.java @@ -0,0 +1,215 @@ +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); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/adapters/SuggestionsAdapter.java b/app/src/main/java/awais/instagrabber/adapters/SuggestionsAdapter.java deleted file mode 100755 index 6c51f1f7..00000000 --- a/app/src/main/java/awais/instagrabber/adapters/SuggestionsAdapter.java +++ /dev/null @@ -1,77 +0,0 @@ -package awais.instagrabber.adapters; - -import android.content.Context; -import android.database.Cursor; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.cursoradapter.widget.CursorAdapter; - -import awais.instagrabber.databinding.ItemSuggestionBinding; -import awais.instagrabber.models.enums.SuggestionType; - -public final class SuggestionsAdapter extends CursorAdapter { - private static final String TAG = "SuggestionsAdapter"; - - private final OnSuggestionClickListener onSuggestionClickListener; - - public SuggestionsAdapter(final Context context, - final OnSuggestionClickListener onSuggestionClickListener) { - super(context, null, FLAG_REGISTER_CONTENT_OBSERVER); - this.onSuggestionClickListener = onSuggestionClickListener; - } - - @Override - public View newView(final Context context, final Cursor cursor, final ViewGroup parent) { - final LayoutInflater layoutInflater = LayoutInflater.from(context); - final ItemSuggestionBinding binding = ItemSuggestionBinding.inflate(layoutInflater, parent, false); - return binding.getRoot(); - // return layoutInflater.inflate(R.layout.item_suggestion, parent, false); - } - - @Override - public void bindView(@NonNull final View view, final Context context, @NonNull final Cursor cursor) { - // i, username, fullname, type, query, picUrl, verified - // 0, 1 , 2 , 3 , 4 , 5 , 6 - final String fullName = cursor.getString(2); - String username = cursor.getString(1); - String picUrl = cursor.getString(5); - final boolean verified = cursor.getString(6).charAt(0) == 't'; - - final String type = cursor.getString(3); - SuggestionType suggestionType = null; - try { - suggestionType = SuggestionType.valueOf(type); - } catch (IllegalArgumentException e) { - Log.e(TAG, "Unknown suggestion type: " + type, e); - } - if (suggestionType == null) return; - String query = cursor.getString(4); - switch (suggestionType) { - case TYPE_USER: - username = '@' + username; - break; - case TYPE_HASHTAG: - username = '#' + username; - break; - } - - if (onSuggestionClickListener != null) { - final SuggestionType finalSuggestionType = suggestionType; - view.setOnClickListener(v -> onSuggestionClickListener.onSuggestionClick(finalSuggestionType, query)); - } - final ItemSuggestionBinding binding = ItemSuggestionBinding.bind(view); - binding.isVerified.setVisibility(verified ? View.VISIBLE : View.GONE); - binding.tvUsername.setText(username); - binding.tvFullName.setVisibility(View.VISIBLE); - binding.tvFullName.setText(fullName); - binding.ivProfilePic.setImageURI(picUrl); - } - - public interface OnSuggestionClickListener { - void onSuggestionClick(final SuggestionType type, final String query); - } -} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/FavoriteViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/FavoriteViewHolder.java index e16824ea..bf9da891 100644 --- a/app/src/main/java/awais/instagrabber/adapters/viewholder/FavoriteViewHolder.java +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/FavoriteViewHolder.java @@ -6,7 +6,7 @@ import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import awais.instagrabber.adapters.FavoritesAdapter; -import awais.instagrabber.databinding.ItemSuggestionBinding; +import awais.instagrabber.databinding.ItemSearchResultBinding; import awais.instagrabber.db.entities.Favorite; import awais.instagrabber.models.enums.FavoriteType; import awais.instagrabber.utils.Constants; @@ -14,12 +14,12 @@ import awais.instagrabber.utils.Constants; public class FavoriteViewHolder extends RecyclerView.ViewHolder { private static final String TAG = "FavoriteViewHolder"; - private final ItemSuggestionBinding binding; + private final ItemSearchResultBinding binding; - public FavoriteViewHolder(@NonNull final ItemSuggestionBinding binding) { + public FavoriteViewHolder(@NonNull final ItemSearchResultBinding binding) { super(binding.getRoot()); this.binding = binding; - binding.isVerified.setVisibility(View.GONE); + binding.verified.setVisibility(View.GONE); } public void bind(final Favorite model, @@ -36,12 +36,12 @@ public class FavoriteViewHolder extends RecyclerView.ViewHolder { return longClickListener.onLongClick(model); }); if (model.getType() == FavoriteType.HASHTAG) { - binding.ivProfilePic.setImageURI(Constants.DEFAULT_HASH_TAG_PIC); + binding.profilePic.setImageURI(Constants.DEFAULT_HASH_TAG_PIC); } else { - binding.ivProfilePic.setImageURI(model.getPicUrl()); + binding.profilePic.setImageURI(model.getPicUrl()); } - binding.tvFullName.setText(model.getDisplayName()); - binding.tvUsername.setVisibility(View.VISIBLE); + binding.title.setVisibility(View.VISIBLE); + binding.subtitle.setText(model.getDisplayName()); String query = model.getQuery(); switch (model.getType()) { case HASHTAG: @@ -51,11 +51,11 @@ public class FavoriteViewHolder extends RecyclerView.ViewHolder { query = "@" + query; break; case LOCATION: - binding.tvUsername.setVisibility(View.GONE); + binding.title.setVisibility(View.GONE); break; default: // do nothing } - binding.tvUsername.setText(query); + binding.title.setText(query); } } diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/SearchItemViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/SearchItemViewHolder.java new file mode 100644 index 00000000..4eb93fed --- /dev/null +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/SearchItemViewHolder.java @@ -0,0 +1,80 @@ +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.getSubtitle(); + 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); + } + }); + } + binding.title.setText(title); + binding.subtitle.setText(subtitle); + binding.profilePic.setImageURI(picUrl); + binding.verified.setVisibility(isVerified ? View.VISIBLE : View.GONE); + } +} diff --git a/app/src/main/java/awais/instagrabber/db/AppDatabase.java b/app/src/main/java/awais/instagrabber/db/AppDatabase.java index 63e2ce43..37446d9d 100644 --- a/app/src/main/java/awais/instagrabber/db/AppDatabase.java +++ b/app/src/main/java/awais/instagrabber/db/AppDatabase.java @@ -22,14 +22,16 @@ import java.util.List; import awais.instagrabber.db.dao.AccountDao; import awais.instagrabber.db.dao.DMLastNotifiedDao; import awais.instagrabber.db.dao.FavoriteDao; +import awais.instagrabber.db.dao.RecentSearchDao; import awais.instagrabber.db.entities.Account; import awais.instagrabber.db.entities.DMLastNotified; import awais.instagrabber.db.entities.Favorite; +import awais.instagrabber.db.entities.RecentSearch; import awais.instagrabber.models.enums.FavoriteType; import awais.instagrabber.utils.Utils; -@Database(entities = {Account.class, Favorite.class, DMLastNotified.class}, - version = 5) +@Database(entities = {Account.class, Favorite.class, DMLastNotified.class, RecentSearch.class}, + version = 6) @TypeConverters({Converters.class}) public abstract class AppDatabase extends RoomDatabase { private static final String TAG = AppDatabase.class.getSimpleName(); @@ -42,12 +44,14 @@ public abstract class AppDatabase extends RoomDatabase { public abstract DMLastNotifiedDao dmLastNotifiedDao(); + public abstract RecentSearchDao recentSearchDao(); + public static AppDatabase getDatabase(final Context context) { if (INSTANCE == null) { synchronized (AppDatabase.class) { if (INSTANCE == null) { INSTANCE = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, "cookiebox.db") - .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5) + .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6) .build(); } } @@ -156,6 +160,21 @@ public abstract class AppDatabase extends RoomDatabase { } }; + static final Migration MIGRATION_5_6 = new Migration(5, 6) { + @Override + public void migrate(@NonNull final SupportSQLiteDatabase database) { + database.execSQL("CREATE TABLE IF NOT EXISTS `recent_searches` (" + + "`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)"); + database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_recent_searches_ig_id_type` ON `recent_searches` (`ig_id`, `type`)"); + } + }; + @NonNull private static List backupOldFavorites(@NonNull final SupportSQLiteDatabase db) { // check if old favorites table had the column query_display diff --git a/app/src/main/java/awais/instagrabber/db/dao/RecentSearchDao.java b/app/src/main/java/awais/instagrabber/db/dao/RecentSearchDao.java new file mode 100644 index 00000000..94266524 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/db/dao/RecentSearchDao.java @@ -0,0 +1,37 @@ +package awais.instagrabber.db.dao; + +import androidx.room.Dao; +import androidx.room.Delete; +import androidx.room.Insert; +import androidx.room.Query; +import androidx.room.Update; + +import java.util.List; + +import awais.instagrabber.db.entities.RecentSearch; +import awais.instagrabber.models.enums.FavoriteType; + +@Dao +public interface RecentSearchDao { + + @Query("SELECT * FROM recent_searches ORDER BY last_searched_on DESC") + List getAllRecentSearches(); + + @Query("SELECT * FROM recent_searches WHERE `ig_id` = :igId AND `type` = :type") + RecentSearch getRecentSearchByIgIdAndType(String igId, FavoriteType type); + + @Query("SELECT * FROM recent_searches WHERE instr(`name`, :query) > 0") + List findRecentSearchesWithNameContaining(String query); + + @Insert + Long insertRecentSearch(RecentSearch recentSearch); + + @Update + void updateRecentSearch(RecentSearch recentSearch); + + @Delete + void deleteRecentSearch(RecentSearch recentSearch); + + // @Query("DELETE from recent_searches") + // void deleteAllRecentSearches(); +} diff --git a/app/src/main/java/awais/instagrabber/db/datasources/RecentSearchDataSource.java b/app/src/main/java/awais/instagrabber/db/datasources/RecentSearchDataSource.java new file mode 100644 index 00000000..9fd950a8 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/db/datasources/RecentSearchDataSource.java @@ -0,0 +1,57 @@ +package awais.instagrabber.db.datasources; + +import android.content.Context; + +import androidx.annotation.NonNull; + +import java.util.List; + +import awais.instagrabber.db.AppDatabase; +import awais.instagrabber.db.dao.RecentSearchDao; +import awais.instagrabber.db.entities.RecentSearch; +import awais.instagrabber.models.enums.FavoriteType; + +public class RecentSearchDataSource { + private static final String TAG = RecentSearchDataSource.class.getSimpleName(); + + private static RecentSearchDataSource INSTANCE; + + private final RecentSearchDao recentSearchDao; + + private RecentSearchDataSource(final RecentSearchDao recentSearchDao) { + this.recentSearchDao = recentSearchDao; + } + + public static synchronized RecentSearchDataSource getInstance(@NonNull Context context) { + if (INSTANCE == null) { + synchronized (RecentSearchDataSource.class) { + if (INSTANCE == null) { + final AppDatabase database = AppDatabase.getDatabase(context); + INSTANCE = new RecentSearchDataSource(database.recentSearchDao()); + } + } + } + return INSTANCE; + } + + public RecentSearch getRecentSearchByIgIdAndType(@NonNull final String igId, @NonNull final FavoriteType type) { + return recentSearchDao.getRecentSearchByIgIdAndType(igId, type); + } + + @NonNull + public final List getAllRecentSearches() { + return recentSearchDao.getAllRecentSearches(); + } + + public final void insertOrUpdateRecentSearch(@NonNull final RecentSearch recentSearch) { + if (recentSearch.getId() != 0) { + recentSearchDao.updateRecentSearch(recentSearch); + return; + } + recentSearchDao.insertRecentSearch(recentSearch); + } + + public final void deleteRecentSearch(@NonNull final RecentSearch recentSearch) { + recentSearchDao.deleteRecentSearch(recentSearch); + } +} diff --git a/app/src/main/java/awais/instagrabber/db/entities/RecentSearch.java b/app/src/main/java/awais/instagrabber/db/entities/RecentSearch.java new file mode 100644 index 00000000..95bd368e --- /dev/null +++ b/app/src/main/java/awais/instagrabber/db/entities/RecentSearch.java @@ -0,0 +1,185 @@ +package awais.instagrabber.db.entities; + +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.room.ColumnInfo; +import androidx.room.Entity; +import androidx.room.Ignore; +import androidx.room.Index; +import androidx.room.PrimaryKey; + +import java.time.LocalDateTime; +import java.util.Objects; + +import awais.instagrabber.models.enums.FavoriteType; +import awais.instagrabber.repositories.responses.search.SearchItem; + +@Entity(tableName = RecentSearch.TABLE_NAME, indices = {@Index(value = {RecentSearch.COL_IG_ID, RecentSearch.COL_TYPE}, unique = true)}) +public class RecentSearch { + private static final String TAG = RecentSearch.class.getSimpleName(); + + public static final String TABLE_NAME = "recent_searches"; + private static final String COL_ID = "id"; + public static final String COL_IG_ID = "ig_id"; + private static final String COL_NAME = "name"; + private static final String COL_USERNAME = "username"; + private static final String COL_PIC_URL = "pic_url"; + public static final String COL_TYPE = "type"; + private static final String COL_LAST_SEARCHED_ON = "last_searched_on"; + + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = COL_ID) + private final int id; + + @ColumnInfo(name = COL_IG_ID) + @NonNull + private final String igId; + + @ColumnInfo(name = COL_NAME) + @NonNull + private final String name; + + @ColumnInfo(name = COL_USERNAME) + private final String username; + + @ColumnInfo(name = COL_PIC_URL) + private final String picUrl; + + @ColumnInfo(name = COL_TYPE) + @NonNull + private final FavoriteType type; + + @ColumnInfo(name = COL_LAST_SEARCHED_ON) + @NonNull + private final LocalDateTime lastSearchedOn; + + @Ignore + public RecentSearch(final String igId, + final String name, + final String username, + final String picUrl, + final FavoriteType type, + final LocalDateTime lastSearchedOn) { + this(0, igId, name, username, picUrl, type, lastSearchedOn); + } + + public RecentSearch(final int id, + @NonNull final String igId, + @NonNull final String name, + final String username, + final String picUrl, + @NonNull final FavoriteType type, + @NonNull final LocalDateTime lastSearchedOn) { + this.id = id; + this.igId = igId; + this.name = name; + this.username = username; + this.picUrl = picUrl; + this.type = type; + this.lastSearchedOn = lastSearchedOn; + } + + public int getId() { + return id; + } + + @NonNull + public String getIgId() { + return igId; + } + + @NonNull + public String getName() { + return name; + } + + public String getUsername() { + return username; + } + + public String getPicUrl() { + return picUrl; + } + + @NonNull + public FavoriteType getType() { + return type; + } + + @NonNull + public LocalDateTime getLastSearchedOn() { + return lastSearchedOn; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final RecentSearch that = (RecentSearch) o; + return Objects.equals(igId, that.igId) && + Objects.equals(name, that.name) && + Objects.equals(username, that.username) && + Objects.equals(picUrl, that.picUrl) && + type == that.type && + Objects.equals(lastSearchedOn, that.lastSearchedOn); + } + + @Override + public int hashCode() { + return Objects.hash(igId, name, username, picUrl, type, lastSearchedOn); + } + + @NonNull + @Override + public String toString() { + return "RecentSearch{" + + "id=" + id + + ", igId='" + igId + '\'' + + ", name='" + name + '\'' + + ", username='" + username + '\'' + + ", picUrl='" + picUrl + '\'' + + ", type=" + type + + ", lastSearchedOn=" + lastSearchedOn + + '}'; + } + + @Nullable + public static RecentSearch fromSearchItem(@NonNull final SearchItem searchItem) { + final FavoriteType type = searchItem.getType(); + if (type == null) return null; + try { + final String igId; + final String name; + final String username; + final String picUrl; + switch (type) { + case USER: + igId = String.valueOf(searchItem.getUser().getPk()); + name = searchItem.getUser().getFullName(); + username = searchItem.getUser().getUsername(); + picUrl = searchItem.getUser().getProfilePicUrl(); + break; + case HASHTAG: + igId = searchItem.getHashtag().getId(); + name = searchItem.getHashtag().getName(); + username = null; + picUrl = null; + break; + case LOCATION: + igId = String.valueOf(searchItem.getPlace().getLocation().getPk()); + name = searchItem.getPlace().getTitle(); + username = null; + picUrl = null; + break; + default: + return null; + } + return new RecentSearch(igId, name, username, picUrl, type, LocalDateTime.now()); + } catch (Exception e) { + Log.e(TAG, "fromSearchItem: ", e); + } + return null; + } +} diff --git a/app/src/main/java/awais/instagrabber/db/repositories/RecentSearchRepository.java b/app/src/main/java/awais/instagrabber/db/repositories/RecentSearchRepository.java new file mode 100644 index 00000000..0832a109 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/db/repositories/RecentSearchRepository.java @@ -0,0 +1,124 @@ +package awais.instagrabber.db.repositories; + +import androidx.annotation.NonNull; + +import java.time.LocalDateTime; +import java.util.List; + +import awais.instagrabber.db.datasources.RecentSearchDataSource; +import awais.instagrabber.db.entities.RecentSearch; +import awais.instagrabber.models.enums.FavoriteType; +import awais.instagrabber.utils.AppExecutors; + +public class RecentSearchRepository { + private static final String TAG = RecentSearchRepository.class.getSimpleName(); + + private static RecentSearchRepository instance; + + private final AppExecutors appExecutors; + private final RecentSearchDataSource recentSearchDataSource; + + private RecentSearchRepository(final AppExecutors appExecutors, final RecentSearchDataSource recentSearchDataSource) { + this.appExecutors = appExecutors; + this.recentSearchDataSource = recentSearchDataSource; + } + + public static RecentSearchRepository getInstance(final RecentSearchDataSource recentSearchDataSource) { + if (instance == null) { + instance = new RecentSearchRepository(AppExecutors.getInstance(), recentSearchDataSource); + } + return instance; + } + + public void getRecentSearch(@NonNull final String igId, + @NonNull final FavoriteType type, + final RepositoryCallback callback) { + // request on the I/O thread + appExecutors.diskIO().execute(() -> { + final RecentSearch recentSearch = recentSearchDataSource.getRecentSearchByIgIdAndType(igId, type); + // notify on the main thread + appExecutors.mainThread().execute(() -> { + if (callback == null) return; + if (recentSearch == null) { + callback.onDataNotAvailable(); + return; + } + callback.onSuccess(recentSearch); + }); + }); + } + + public void getAllRecentSearches(final RepositoryCallback> callback) { + // request on the I/O thread + appExecutors.diskIO().execute(() -> { + final List recentSearches = recentSearchDataSource.getAllRecentSearches(); + // notify on the main thread + appExecutors.mainThread().execute(() -> { + if (callback == null) return; + callback.onSuccess(recentSearches); + }); + }); + } + + public void insertOrUpdateRecentSearch(@NonNull final RecentSearch recentSearch, + final RepositoryCallback callback) { + insertOrUpdateRecentSearch(recentSearch.getIgId(), recentSearch.getName(), recentSearch.getUsername(), recentSearch.getPicUrl(), + recentSearch.getType(), callback); + } + + public void insertOrUpdateRecentSearch(@NonNull final String igId, + @NonNull final String name, + final String username, + final String picUrl, + @NonNull final FavoriteType type, + final RepositoryCallback callback) { + // request on the I/O thread + appExecutors.diskIO().execute(() -> { + RecentSearch recentSearch = recentSearchDataSource.getRecentSearchByIgIdAndType(igId, type); + recentSearch = recentSearch == null + ? new RecentSearch(igId, name, username, picUrl, type, LocalDateTime.now()) + : new RecentSearch(recentSearch.getId(), igId, name, username, picUrl, type, LocalDateTime.now()); + recentSearchDataSource.insertOrUpdateRecentSearch(recentSearch); + // notify on the main thread + appExecutors.mainThread().execute(() -> { + if (callback == null) return; + callback.onSuccess(null); + }); + }); + } + + public void deleteRecentSearchByIgIdAndType(@NonNull final String igId, + @NonNull final FavoriteType type, + final RepositoryCallback callback) { + // request on the I/O thread + appExecutors.diskIO().execute(() -> { + final RecentSearch recentSearch = recentSearchDataSource.getRecentSearchByIgIdAndType(igId, type); + if (recentSearch != null) { + recentSearchDataSource.deleteRecentSearch(recentSearch); + } + // notify on the main thread + appExecutors.mainThread().execute(() -> { + if (callback == null) return; + if (recentSearch == null) { + callback.onDataNotAvailable(); + return; + } + callback.onSuccess(null); + }); + }); + } + + public void deleteRecentSearch(@NonNull final RecentSearch recentSearch, + final RepositoryCallback callback) { + // request on the I/O thread + appExecutors.diskIO().execute(() -> { + + recentSearchDataSource.deleteRecentSearch(recentSearch); + // notify on the main thread + appExecutors.mainThread().execute(() -> { + if (callback == null) return; + callback.onSuccess(null); + }); + }); + } +} diff --git a/app/src/main/java/awais/instagrabber/fragments/FavoritesFragment.java b/app/src/main/java/awais/instagrabber/fragments/FavoritesFragment.java index 7be4156c..975357e8 100644 --- a/app/src/main/java/awais/instagrabber/fragments/FavoritesFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/FavoritesFragment.java @@ -41,7 +41,9 @@ public class FavoritesFragment extends Fragment { @Override public void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - favoriteRepository = FavoriteRepository.getInstance(FavoriteDataSource.getInstance(getContext())); + final Context context = getContext(); + if (context == null) return; + favoriteRepository = FavoriteRepository.getInstance(FavoriteDataSource.getInstance(context)); } @NonNull diff --git a/app/src/main/java/awais/instagrabber/fragments/search/SearchCategoryFragment.java b/app/src/main/java/awais/instagrabber/fragments/search/SearchCategoryFragment.java new file mode 100644 index 00000000..89a2c7e9 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/fragments/search/SearchCategoryFragment.java @@ -0,0 +1,195 @@ +package awais.instagrabber.fragments.search; + +import android.content.Context; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + +import java.io.Serializable; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import awais.instagrabber.adapters.SearchItemsAdapter; +import awais.instagrabber.models.Resource; +import awais.instagrabber.models.enums.FavoriteType; +import awais.instagrabber.repositories.responses.search.SearchItem; +import awais.instagrabber.viewmodels.SearchFragmentViewModel; + +public class SearchCategoryFragment extends Fragment { + private static final String TAG = SearchCategoryFragment.class.getSimpleName(); + private static final String ARG_TYPE = "type"; + + + @Nullable + private SwipeRefreshLayout swipeRefreshLayout; + @Nullable + private RecyclerView list; + private SearchFragmentViewModel viewModel; + private FavoriteType type; + private SearchItemsAdapter searchItemsAdapter; + @Nullable + private OnSearchItemClickListener onSearchItemClickListener; + private boolean skipViewRefresh; + private String prevQuery; + + @NonNull + public static SearchCategoryFragment newInstance(@NonNull final FavoriteType type) { + final SearchCategoryFragment fragment = new SearchCategoryFragment(); + final Bundle args = new Bundle(); + args.putSerializable(ARG_TYPE, type); + fragment.setArguments(args); + return fragment; + } + + public SearchCategoryFragment() {} + + @Override + public void onAttach(@NonNull final Context context) { + super.onAttach(context); + final Fragment parentFragment = getParentFragment(); + if (!(parentFragment instanceof OnSearchItemClickListener)) return; + onSearchItemClickListener = (OnSearchItemClickListener) parentFragment; + } + + @Override + public void onCreate(@Nullable final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + final FragmentActivity fragmentActivity = getActivity(); + if (fragmentActivity == null) return; + viewModel = new ViewModelProvider(fragmentActivity).get(SearchFragmentViewModel.class); + final Bundle args = getArguments(); + if (args == null) { + Log.e(TAG, "onCreate: arguments are null"); + return; + } + final Serializable typeSerializable = args.getSerializable(ARG_TYPE); + if (!(typeSerializable instanceof FavoriteType)) { + Log.e(TAG, "onCreate: type not a FavoriteType"); + return; + } + type = (FavoriteType) typeSerializable; + } + + @Nullable + @Override + public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) { + final Context context = getContext(); + if (context == null) return null; + skipViewRefresh = false; + if (swipeRefreshLayout != null) { + skipViewRefresh = true; + return swipeRefreshLayout; + } + swipeRefreshLayout = new SwipeRefreshLayout(context); + swipeRefreshLayout.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + list = new RecyclerView(context); + list.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + swipeRefreshLayout.addView(list); + return swipeRefreshLayout; + } + + @Override + public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { + if (skipViewRefresh) return; + setupList(); + } + + @Override + public void onResume() { + super.onResume(); + // Log.d(TAG, "onResume: type: " + type); + setupObservers(); + final String currentQuery = viewModel.getQuery().getValue(); + if (prevQuery != null && currentQuery != null && !Objects.equals(prevQuery, currentQuery)) { + viewModel.search(currentQuery, type); + } + prevQuery = null; + } + + private void setupList() { + if (list == null || swipeRefreshLayout == null) return; + final Context context = getContext(); + if (context == null) return; + list.setLayoutManager(new LinearLayoutManager(context)); + searchItemsAdapter = new SearchItemsAdapter(onSearchItemClickListener); + list.setAdapter(searchItemsAdapter); + swipeRefreshLayout.setOnRefreshListener(() -> { + String currentQuery = viewModel.getQuery().getValue(); + if (currentQuery == null) currentQuery = ""; + viewModel.search(currentQuery, type); + }); + } + + private void setupObservers() { + viewModel.getQuery().observe(getViewLifecycleOwner(), q -> { + if (!isVisible() || Objects.equals(prevQuery, q)) return; + viewModel.search(q, type); + prevQuery = q; + }); + final LiveData>> resultsLiveData = getResultsLiveData(); + if (resultsLiveData != null) { + resultsLiveData.observe(getViewLifecycleOwner(), this::onResults); + } + } + + private void onResults(final Resource> listResource) { + if (listResource == null) return; + switch (listResource.status) { + case SUCCESS: + if (searchItemsAdapter != null) { + searchItemsAdapter.submitList(listResource.data); + } + if (swipeRefreshLayout != null) { + swipeRefreshLayout.setRefreshing(false); + } + break; + case ERROR: + if (searchItemsAdapter != null) { + searchItemsAdapter.submitList(Collections.emptyList()); + } + if (swipeRefreshLayout != null) { + swipeRefreshLayout.setRefreshing(false); + } + break; + case LOADING: + if (swipeRefreshLayout != null) { + swipeRefreshLayout.setRefreshing(true); + } + break; + } + } + + @Nullable + private LiveData>> getResultsLiveData() { + switch (type) { + case TOP: + return viewModel.getTopResults(); + case USER: + return viewModel.getUserResults(); + case HASHTAG: + return viewModel.getHashtagResults(); + case LOCATION: + return viewModel.getLocationResults(); + } + return null; + } + + public interface OnSearchItemClickListener { + void onSearchItemClick(SearchItem searchItem); + + void onSearchItemDelete(SearchItem searchItem); + } +} diff --git a/app/src/main/java/awais/instagrabber/fragments/search/SearchFragment.java b/app/src/main/java/awais/instagrabber/fragments/search/SearchFragment.java new file mode 100644 index 00000000..91ca1519 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/fragments/search/SearchFragment.java @@ -0,0 +1,245 @@ +package awais.instagrabber.fragments.search; + +import android.os.Bundle; +import android.text.Editable; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.LinearLayoutCompat; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.Observer; +import androidx.lifecycle.ViewModelProvider; +import androidx.navigation.NavController; +import androidx.navigation.fragment.NavHostFragment; + +import com.google.android.material.snackbar.Snackbar; +import com.google.android.material.tabs.TabLayoutMediator; +import com.google.android.material.textfield.TextInputLayout; + +import java.util.Arrays; +import java.util.List; + +import awais.instagrabber.R; +import awais.instagrabber.activities.MainActivity; +import awais.instagrabber.adapters.SearchCategoryAdapter; +import awais.instagrabber.customviews.helpers.TextWatcherAdapter; +import awais.instagrabber.databinding.FragmentSearchBinding; +import awais.instagrabber.models.Resource; +import awais.instagrabber.models.enums.FavoriteType; +import awais.instagrabber.repositories.responses.search.SearchItem; +import awais.instagrabber.viewmodels.SearchFragmentViewModel; + +public class SearchFragment extends Fragment implements SearchCategoryFragment.OnSearchItemClickListener { + private static final String TAG = SearchFragment.class.getSimpleName(); + private static final String QUERY = "query"; + + private FragmentSearchBinding binding; + private LinearLayoutCompat root; + private boolean shouldRefresh = true; + @Nullable + private TextInputLayout searchInputLayout; + @Nullable + private EditText searchInput; + @Nullable + private MainActivity mainActivity; + private SearchFragmentViewModel viewModel; + + private final TextWatcherAdapter textWatcher = new TextWatcherAdapter() { + @Override + public void afterTextChanged(final Editable s) { + if (s == null) return; + viewModel.submitQuery(s.toString().trim()); + } + }; + + @Override + public void onCreate(@Nullable final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + final FragmentActivity fragmentActivity = getActivity(); + if (!(fragmentActivity instanceof MainActivity)) return; + mainActivity = (MainActivity) fragmentActivity; + viewModel = new ViewModelProvider(mainActivity).get(SearchFragmentViewModel.class); + } + + @Nullable + @Override + public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) { + if (root != null) { + shouldRefresh = false; + return root; + } + binding = FragmentSearchBinding.inflate(inflater, container, false); + root = binding.getRoot(); + return root; + } + + @Override + public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { + if (!shouldRefresh) return; + init(savedInstanceState); + shouldRefresh = false; + } + + @Override + public void onSaveInstanceState(@NonNull final Bundle outState) { + super.onSaveInstanceState(outState); + final String current = viewModel.getQuery().getValue(); + if (TextUtils.isEmpty(current)) return; + outState.putString(QUERY, current); + } + + @Override + public void onPause() { + super.onPause(); + if (mainActivity != null) { + mainActivity.hideSearchView(); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (mainActivity != null) { + mainActivity.hideSearchView(); + } + if (searchInput != null) { + searchInput.removeTextChangedListener(textWatcher); + searchInput.setText(""); + } + } + + @Override + public void onResume() { + super.onResume(); + if (mainActivity != null) { + mainActivity.showSearchView(); + } + if (searchInputLayout != null) { + searchInputLayout.requestFocus(); + } + } + + private void init(@Nullable final Bundle savedInstanceState) { + if (mainActivity == null) return; + searchInputLayout = mainActivity.showSearchView(); + searchInput = searchInputLayout.getEditText(); + setupObservers(); + setupViewPager(); + setupSearchInput(savedInstanceState); + } + + private void setupObservers() { + viewModel.getQuery().observe(getViewLifecycleOwner(), q -> {}); // need to observe, so that getQuery returns proper value + } + + private void setupSearchInput(@Nullable final Bundle savedInstanceState) { + if (searchInput == null) return; + searchInput.removeTextChangedListener(textWatcher); // make sure we add only 1 instance of textWatcher + searchInput.addTextChangedListener(textWatcher); + boolean triggerEmptyQuery = true; + if (savedInstanceState != null) { + final String savedQuery = savedInstanceState.getString(QUERY); + if (TextUtils.isEmpty(savedQuery)) return; + searchInput.setText(savedQuery); + triggerEmptyQuery = false; + } + searchInput.requestFocus(); + if (triggerEmptyQuery) { + viewModel.submitQuery(""); + } + } + + private void setupViewPager() { + binding.pager.setSaveEnabled(false); + final List categories = Arrays.asList(FavoriteType.values()); + binding.pager.setAdapter(new SearchCategoryAdapter(this, categories)); + final TabLayoutMediator mediator = new TabLayoutMediator(binding.tabLayout, binding.pager, (tab, position) -> { + try { + final FavoriteType type = categories.get(position); + final int resId; + switch (type) { + case TOP: + resId = R.string.top; + break; + case USER: + resId = R.string.accounts; + break; + case HASHTAG: + resId = R.string.hashtags; + break; + case LOCATION: + resId = R.string.locations; + break; + default: + throw new IllegalStateException("Unexpected value: " + type); + } + tab.setText(resId); + } catch (Exception e) { + Log.e(TAG, "setupViewPager: ", e); + } + }); + mediator.attach(); + } + + @Override + public void onSearchItemClick(final SearchItem searchItem) { + if (searchItem == null) return; + final FavoriteType type = searchItem.getType(); + if (type == null) return; + try { + if (!searchItem.isFavorite()) { + viewModel.saveToRecentSearches(searchItem); // insert or update recent + } + final NavController navController = NavHostFragment.findNavController(this); + final Bundle bundle = new Bundle(); + switch (type) { + case USER: + bundle.putString("username", searchItem.getUser().getUsername()); + navController.navigate(R.id.action_global_profileFragment, bundle); + break; + case HASHTAG: + bundle.putString("hashtag", searchItem.getHashtag().getName()); + navController.navigate(R.id.action_global_hashTagFragment, bundle); + break; + case LOCATION: + bundle.putLong("locationId", searchItem.getPlace().getLocation().getPk()); + navController.navigate(R.id.action_global_locationFragment, bundle); + break; + } + } catch (Exception e) { + Log.e(TAG, "onSearchItemClick: ", e); + } + } + + @Override + public void onSearchItemDelete(final SearchItem searchItem) { + final LiveData> liveData = viewModel.deleteRecentSearch(searchItem); + if (liveData == null) return; + liveData.observe(getViewLifecycleOwner(), new Observer>() { + @Override + public void onChanged(final Resource resource) { + if (resource == null) return; + switch (resource.status) { + case SUCCESS: + viewModel.search("", FavoriteType.TOP); + liveData.removeObserver(this); + break; + case ERROR: + Snackbar.make(binding.getRoot(), R.string.error, Snackbar.LENGTH_SHORT); + liveData.removeObserver(this); + break; + case LOADING: + break; + } + } + }); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/enums/FavoriteType.java b/app/src/main/java/awais/instagrabber/models/enums/FavoriteType.java index cdf926a9..536ac05f 100644 --- a/app/src/main/java/awais/instagrabber/models/enums/FavoriteType.java +++ b/app/src/main/java/awais/instagrabber/models/enums/FavoriteType.java @@ -1,7 +1,8 @@ package awais.instagrabber.models.enums; public enum FavoriteType { + TOP, // used just for searching USER, HASHTAG, - LOCATION + LOCATION, } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/Hashtag.java b/app/src/main/java/awais/instagrabber/repositories/responses/Hashtag.java index 2ce08eb5..f1ef47b3 100755 --- a/app/src/main/java/awais/instagrabber/repositories/responses/Hashtag.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/Hashtag.java @@ -1,6 +1,7 @@ package awais.instagrabber.repositories.responses; import java.io.Serializable; +import java.util.Objects; import awais.instagrabber.models.enums.FollowingType; @@ -42,4 +43,21 @@ public final class Hashtag implements Serializable { public String getSubtitle() { return searchResultSubtitle; } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final Hashtag hashtag = (Hashtag) o; + return mediaCount == hashtag.mediaCount && + following == hashtag.following && + Objects.equals(id, hashtag.id) && + Objects.equals(name, hashtag.name) && + Objects.equals(searchResultSubtitle, hashtag.searchResultSubtitle); + } + + @Override + public int hashCode() { + return Objects.hash(following, mediaCount, id, name, searchResultSubtitle); + } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/Place.java b/app/src/main/java/awais/instagrabber/repositories/responses/Place.java index c72e1c41..3f10ffd4 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/Place.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/Place.java @@ -1,5 +1,7 @@ package awais.instagrabber.repositories.responses; +import java.util.Objects; + public class Place { private final Location location; // for search @@ -40,4 +42,21 @@ public class Place { public String getStatus() { return status; } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final Place place = (Place) o; + return Objects.equals(location, place.location) && + Objects.equals(title, place.title) && + Objects.equals(subtitle, place.subtitle) && + Objects.equals(slug, place.slug) && + Objects.equals(status, place.status); + } + + @Override + public int hashCode() { + return Objects.hash(location, title, subtitle, slug, status); + } } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/search/SearchItem.java b/app/src/main/java/awais/instagrabber/repositories/responses/search/SearchItem.java index 4aca79da..296e9c84 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/search/SearchItem.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/search/SearchItem.java @@ -1,15 +1,34 @@ package awais.instagrabber.repositories.responses.search; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import awais.instagrabber.db.entities.Favorite; +import awais.instagrabber.db.entities.RecentSearch; +import awais.instagrabber.models.enums.FavoriteType; import awais.instagrabber.repositories.responses.Hashtag; +import awais.instagrabber.repositories.responses.Location; import awais.instagrabber.repositories.responses.Place; import awais.instagrabber.repositories.responses.User; public class SearchItem { + private static final String TAG = SearchItem.class.getSimpleName(); + private final User user; private final Place place; private final Hashtag hashtag; private final int position; + private boolean isRecent = false; + private boolean isFavorite = false; + public SearchItem(final User user, final Place place, final Hashtag hashtag, @@ -35,4 +54,221 @@ public class SearchItem { public int getPosition() { return position; } + + public boolean isRecent() { + return isRecent; + } + + public void setRecent(final boolean recent) { + isRecent = recent; + } + + public boolean isFavorite() { + return isFavorite; + } + + public void setFavorite(final boolean favorite) { + isFavorite = favorite; + } + + @Nullable + public FavoriteType getType() { + if (user != null) { + return FavoriteType.USER; + } + if (hashtag != null) { + return FavoriteType.HASHTAG; + } + if (place != null) { + return FavoriteType.LOCATION; + } + return null; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final SearchItem that = (SearchItem) o; + return Objects.equals(user, that.user) && + Objects.equals(place, that.place) && + Objects.equals(hashtag, that.hashtag); + } + + @Override + public int hashCode() { + return Objects.hash(user, place, hashtag); + } + + @NonNull + @Override + public String toString() { + return "SearchItem{" + + "user=" + user + + ", place=" + place + + ", hashtag=" + hashtag + + ", position=" + position + + ", isRecent=" + isRecent + + '}'; + } + + @NonNull + public static List fromRecentSearch(final List recentSearches) { + if (recentSearches == null) return Collections.emptyList(); + return recentSearches.stream() + .map(SearchItem::fromRecentSearch) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + @Nullable + private static SearchItem fromRecentSearch(final RecentSearch recentSearch) { + if (recentSearch == null) return null; + try { + final FavoriteType type = recentSearch.getType(); + final SearchItem searchItem; + switch (type) { + case USER: + searchItem = new SearchItem(getUser(recentSearch), null, null, 0); + break; + case HASHTAG: + searchItem = new SearchItem(null, null, getHashtag(recentSearch), 0); + break; + case LOCATION: + searchItem = new SearchItem(null, getPlace(recentSearch), null, 0); + break; + default: + return null; + } + searchItem.setRecent(true); + return searchItem; + } catch (Exception e) { + Log.e(TAG, "fromRecentSearch: ", e); + } + return null; + } + + @NonNull + private static User getUser(@NonNull final RecentSearch recentSearch) { + return new User( + Long.parseLong(recentSearch.getIgId()), + recentSearch.getUsername(), + recentSearch.getName(), + false, + recentSearch.getPicUrl(), + null, null, false, false, false, false, false, + null, null, 0, 0, 0, 0, null, null, + 0, null, null, null, null, null, null + ); + } + + @NonNull + private static Hashtag getHashtag(@NonNull final RecentSearch recentSearch) { + return new Hashtag( + recentSearch.getIgId(), + recentSearch.getName(), + 0, + null, + null + ); + } + + @NonNull + private static Place getPlace(@NonNull final RecentSearch recentSearch) { + final Location location = new Location( + Long.parseLong(recentSearch.getIgId()), + recentSearch.getName(), + recentSearch.getName(), + null, null, 0, 0 + ); + return new Place( + location, + recentSearch.getName(), + null, + null, + null + ); + } + + public static List fromFavorite(final List favorites) { + if (favorites == null) { + return Collections.emptyList(); + } + return favorites.stream() + .map(SearchItem::fromFavorite) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + @Nullable + private static SearchItem fromFavorite(final Favorite favorite) { + if (favorite == null) return null; + final FavoriteType type = favorite.getType(); + if (type == null) return null; + final SearchItem searchItem; + switch (type) { + case USER: + searchItem = new SearchItem(getUser(favorite), null, null, 0); + break; + case HASHTAG: + searchItem = new SearchItem(null, null, getHashtag(favorite), 0); + break; + case LOCATION: + final Place place = getPlace(favorite); + if (place == null) return null; + searchItem = new SearchItem(null, place, null, 0); + break; + default: + return null; + } + searchItem.setFavorite(true); + return searchItem; + } + + @NonNull + private static User getUser(@NonNull final Favorite favorite) { + return new User( + 0, + favorite.getQuery(), + favorite.getDisplayName(), + false, + favorite.getPicUrl(), + null, null, false, false, false, false, false, + null, null, 0, 0, 0, 0, null, null, + 0, null, null, null, null, null, null + ); + } + + @NonNull + private static Hashtag getHashtag(@NonNull final Favorite favorite) { + return new Hashtag( + "0", + favorite.getQuery(), + 0, + null, + null + ); + } + + @Nullable + private static Place getPlace(@NonNull final Favorite favorite) { + try { + final Location location = new Location( + Long.parseLong(favorite.getQuery()), + favorite.getDisplayName(), + favorite.getDisplayName(), + null, null, 0, 0 + ); + return new Place( + location, + favorite.getDisplayName(), + null, + null, + null + ); + } catch (Exception e) { + Log.e(TAG, "getPlace: ", e); + return null; + } + } } diff --git a/app/src/main/java/awais/instagrabber/viewmodels/AppStateViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/AppStateViewModel.java index ab1cbabb..84fd2ba9 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/AppStateViewModel.java +++ b/app/src/main/java/awais/instagrabber/viewmodels/AppStateViewModel.java @@ -8,8 +8,6 @@ import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; -import awais.instagrabber.db.datasources.AccountDataSource; -import awais.instagrabber.db.repositories.AccountRepository; import awais.instagrabber.repositories.responses.User; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.CookieUtils; @@ -23,20 +21,18 @@ public class AppStateViewModel extends AndroidViewModel { private static final String TAG = AppStateViewModel.class.getSimpleName(); private final String cookie; - private final boolean isLoggedIn; private final MutableLiveData currentUser = new MutableLiveData<>(); - private AccountRepository accountRepository; private UserService userService; public AppStateViewModel(@NonNull final Application application) { super(application); // Log.d(TAG, "AppStateViewModel: constructor"); cookie = settingsHelper.getString(Constants.COOKIE); - isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) > 0; + final boolean isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) != 0; if (!isLoggedIn) return; userService = UserService.getInstance(); - accountRepository = AccountRepository.getInstance(AccountDataSource.getInstance(application)); + // final AccountRepository accountRepository = AccountRepository.getInstance(AccountDataSource.getInstance(application)); fetchProfileDetails(); } @@ -50,6 +46,7 @@ public class AppStateViewModel extends AndroidViewModel { private void fetchProfileDetails() { final long uid = CookieUtils.getUserIdFromCookie(cookie); + if (userService == null) return; userService.getUserInfo(uid, new ServiceCallback() { @Override public void onSuccess(final User user) { diff --git a/app/src/main/java/awais/instagrabber/viewmodels/SearchFragmentViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/SearchFragmentViewModel.java new file mode 100644 index 00000000..229cc9c1 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/viewmodels/SearchFragmentViewModel.java @@ -0,0 +1,352 @@ +package awais.instagrabber.viewmodels; + +import android.app.Application; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import awais.instagrabber.db.datasources.FavoriteDataSource; +import awais.instagrabber.db.datasources.RecentSearchDataSource; +import awais.instagrabber.db.entities.Favorite; +import awais.instagrabber.db.entities.RecentSearch; +import awais.instagrabber.db.repositories.FavoriteRepository; +import awais.instagrabber.db.repositories.RecentSearchRepository; +import awais.instagrabber.db.repositories.RepositoryCallback; +import awais.instagrabber.models.Resource; +import awais.instagrabber.models.enums.FavoriteType; +import awais.instagrabber.repositories.responses.search.SearchItem; +import awais.instagrabber.repositories.responses.search.SearchResponse; +import awais.instagrabber.utils.AppExecutors; +import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.CookieUtils; +import awais.instagrabber.utils.Debouncer; +import awais.instagrabber.utils.TextUtils; +import awais.instagrabber.webservices.SearchService; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +import static androidx.lifecycle.Transformations.distinctUntilChanged; +import static awais.instagrabber.utils.Utils.settingsHelper; + +public class SearchFragmentViewModel extends AppStateViewModel { + private static final String TAG = SearchFragmentViewModel.class.getSimpleName(); + private static final String QUERY = "query"; + + private final MutableLiveData query = new MutableLiveData<>(); + private final MutableLiveData>> topResults = new MutableLiveData<>(); + private final MutableLiveData>> userResults = new MutableLiveData<>(); + private final MutableLiveData>> hashtagResults = new MutableLiveData<>(); + private final MutableLiveData>> locationResults = new MutableLiveData<>(); + + private final SearchService searchService; + private final Debouncer searchDebouncer; + private final boolean isLoggedIn; + private final LiveData distinctQuery; + private final RecentSearchRepository recentSearchRepository; + private final FavoriteRepository favoriteRepository; + + private String tempQuery; + + public SearchFragmentViewModel(@NonNull final Application application) { + super(application); + final String cookie = settingsHelper.getString(Constants.COOKIE); + isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) != 0; + final Debouncer.Callback searchCallback = new Debouncer.Callback() { + @Override + public void call(final String key) { + if (tempQuery == null) return; + query.postValue(tempQuery); + } + + @Override + public void onError(final Throwable t) { + Log.e(TAG, "onError: ", t); + } + }; + searchDebouncer = new Debouncer<>(searchCallback, 500); + distinctQuery = distinctUntilChanged(query); + searchService = SearchService.getInstance(); + recentSearchRepository = RecentSearchRepository.getInstance(RecentSearchDataSource.getInstance(application)); + favoriteRepository = FavoriteRepository.getInstance(FavoriteDataSource.getInstance(application)); + } + + public LiveData getQuery() { + return distinctQuery; + } + + public LiveData>> getTopResults() { + return topResults; + } + + public LiveData>> getUserResults() { + return userResults; + } + + public LiveData>> getHashtagResults() { + return hashtagResults; + } + + public LiveData>> getLocationResults() { + return locationResults; + } + + public void submitQuery(@Nullable final String query) { + String localQuery = query; + if (query == null) { + localQuery = ""; + } + if (tempQuery != null && Objects.equals(localQuery.toLowerCase(), tempQuery.toLowerCase())) return; + tempQuery = query; + if (TextUtils.isEmpty(query)) { + // If empty immediately post it + searchDebouncer.cancel(QUERY); + this.query.postValue(""); + return; + } + searchDebouncer.call(QUERY); + } + + public void search(@NonNull final String query, + @NonNull final FavoriteType type) { + final MutableLiveData>> liveData = getLiveDataByType(type); + if (liveData == null) return; + if (TextUtils.isEmpty(query)) { + if (type != FavoriteType.TOP) { + liveData.postValue(Resource.success(Collections.emptyList())); + return; + } + showRecentSearchesAndFavorites(); + return; + } + if (query.equals("@") || query.equals("#")) return; + final String c; + switch (type) { + case TOP: + c = "blended"; + break; + case USER: + c = "user"; + break; + case HASHTAG: + c = "hashtag"; + break; + case LOCATION: + c = "place"; + break; + default: + return; + } + liveData.postValue(Resource.loading(null)); + final Call request = searchService.search(isLoggedIn, query, c); + request.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, + @NonNull final Response response) { + if (!response.isSuccessful()) { + sendErrorResponse(type); + return; + } + final SearchResponse body = response.body(); + if (body == null) { + sendErrorResponse(type); + return; + } + parseResponse(body, type); + } + + @Override + public void onFailure(@NonNull final Call call, + @NonNull final Throwable t) { + Log.e(TAG, "onFailure: ", t); + } + }); + } + + private void showRecentSearchesAndFavorites() { + final SettableFuture> recentResultsFuture = SettableFuture.create(); + final SettableFuture> favoritesFuture = SettableFuture.create(); + recentSearchRepository.getAllRecentSearches(new RepositoryCallback>() { + @Override + public void onSuccess(final List result) { + recentResultsFuture.set(result); + } + + @Override + public void onDataNotAvailable() { + recentResultsFuture.set(Collections.emptyList()); + } + }); + favoriteRepository.getAllFavorites(new RepositoryCallback>() { + @Override + public void onSuccess(final List result) { + favoritesFuture.set(result); + } + + @Override + public void onDataNotAvailable() { + favoritesFuture.set(Collections.emptyList()); + } + }); + //noinspection UnstableApiUsage + final ListenableFuture>> listenableFuture = Futures.allAsList(recentResultsFuture, favoritesFuture); + Futures.addCallback(listenableFuture, new FutureCallback>>() { + @Override + public void onSuccess(@Nullable final List> result) { + if (!TextUtils.isEmpty(tempQuery)) return; // Make sure user has not entered anything before updating results + if (result == null) { + topResults.postValue(Resource.success(Collections.emptyList())); + return; + } + try { + //noinspection unchecked + topResults.postValue(Resource.success( + ImmutableList.builder() + .addAll(SearchItem.fromRecentSearch((List) result.get(0))) + .addAll(SearchItem.fromFavorite((List) result.get(1))) + .build() + )); + } catch (Exception e) { + Log.e(TAG, "onSuccess: ", e); + topResults.postValue(Resource.success(Collections.emptyList())); + } + } + + @Override + public void onFailure(@NonNull final Throwable t) { + if (!TextUtils.isEmpty(tempQuery)) return; + topResults.postValue(Resource.success(Collections.emptyList())); + Log.e(TAG, "onFailure: ", t); + } + }, AppExecutors.getInstance().mainThread()); + } + + private void sendErrorResponse(@NonNull final FavoriteType type) { + final MutableLiveData>> liveData = getLiveDataByType(type); + if (liveData == null) return; + liveData.postValue(Resource.error(null, Collections.emptyList())); + } + + private MutableLiveData>> getLiveDataByType(@NonNull final FavoriteType type) { + final MutableLiveData>> liveData; + switch (type) { + case TOP: + liveData = topResults; + break; + case USER: + liveData = userResults; + break; + case HASHTAG: + liveData = hashtagResults; + break; + case LOCATION: + liveData = locationResults; + break; + default: + return null; + } + return liveData; + } + + private void parseResponse(@NonNull final SearchResponse body, + @NonNull final FavoriteType type) { + final MutableLiveData>> liveData = getLiveDataByType(type); + if (liveData == null) return; + if (isLoggedIn) { + if (body.getList() == null) { + liveData.postValue(Resource.success(Collections.emptyList())); + return; + } + if (type == FavoriteType.HASHTAG || type == FavoriteType.LOCATION) { + liveData.postValue(Resource.success(body.getList() + .stream() + .filter(i -> i.getUser() == null) + .collect(Collectors.toList()))); + return; + } + liveData.postValue(Resource.success(body.getList())); + return; + } + + // anonymous + final List list; + switch (type) { + case TOP: + list = ImmutableList + .builder() + .addAll(body.getUsers()) + .addAll(body.getHashtags()) + .addAll(body.getPlaces()) + .build(); + break; + case USER: + list = body.getUsers(); + break; + case HASHTAG: + list = body.getHashtags(); + break; + case LOCATION: + list = body.getPlaces(); + break; + default: + return; + } + liveData.postValue(Resource.success(list)); + } + + public void saveToRecentSearches(final SearchItem searchItem) { + if (searchItem == null) return; + try { + final RecentSearch recentSearch = RecentSearch.fromSearchItem(searchItem); + if (recentSearch == null) return; + recentSearchRepository.insertOrUpdateRecentSearch(recentSearch, new RepositoryCallback() { + @Override + public void onSuccess(final Void result) { + // Log.d(TAG, "onSuccess: inserted recent: " + recentSearch); + } + + @Override + public void onDataNotAvailable() {} + }); + } catch (Exception e) { + Log.e(TAG, "saveToRecentSearches: ", e); + } + } + + @Nullable + public LiveData> deleteRecentSearch(final SearchItem searchItem) { + if (searchItem == null || !searchItem.isRecent()) return null; + final RecentSearch recentSearch = RecentSearch.fromSearchItem(searchItem); + if (recentSearch == null) return null; + final MutableLiveData> data = new MutableLiveData<>(); + data.postValue(Resource.loading(null)); + recentSearchRepository.deleteRecentSearchByIgIdAndType(recentSearch.getIgId(), recentSearch.getType(), new RepositoryCallback() { + @Override + public void onSuccess(final Void result) { + // Log.d(TAG, "onSuccess: deleted"); + data.postValue(Resource.success(new Object())); + } + + @Override + public void onDataNotAvailable() { + // Log.e(TAG, "onDataNotAvailable: not deleted"); + data.postValue(Resource.error("Error deleting recent item", null)); + } + }); + return data; + } +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 97bcb0bb..8fadd4a2 100755 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -32,7 +32,29 @@ android:background="?attr/colorSurface" app:layout_collapseMode="pin" app:title="@string/app_name" - tools:menu="@menu/main_menu" /> + tools:menu="@menu/main_menu"> + + + + + + diff --git a/app/src/main/res/layout/fragment_favorites.xml b/app/src/main/res/layout/fragment_favorites.xml index 05a808d6..d437e6e7 100644 --- a/app/src/main/res/layout/fragment_favorites.xml +++ b/app/src/main/res/layout/fragment_favorites.xml @@ -4,4 +4,4 @@ android:id="@+id/favorite_list" android:layout_width="match_parent" android:layout_height="match_parent" - tools:listitem="@layout/item_suggestion" /> \ No newline at end of file + tools:listitem="@layout/item_search_result" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml new file mode 100644 index 00000000..39c24c3c --- /dev/null +++ b/app/src/main/res/layout/fragment_search.xml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_suggestion.xml b/app/src/main/res/layout/item_search_result.xml old mode 100755 new mode 100644 similarity index 63% rename from app/src/main/res/layout/item_suggestion.xml rename to app/src/main/res/layout/item_search_result.xml index 8020090a..e6987925 --- a/app/src/main/res/layout/item_suggestion.xml +++ b/app/src/main/res/layout/item_search_result.xml @@ -13,7 +13,7 @@ android:paddingBottom="8dp"> + + \ No newline at end of file diff --git a/app/src/main/res/menu/main_menu.xml b/app/src/main/res/menu/main_menu.xml index 2ad8da42..b73c4b97 100644 --- a/app/src/main/res/menu/main_menu.xml +++ b/app/src/main/res/menu/main_menu.xml @@ -1,25 +1,10 @@ - - - - - - - - - - - - - - + android:title="@string/search" + app:showAsAction="always" /> \ No newline at end of file diff --git a/app/src/main/res/navigation/direct_messages_nav_graph.xml b/app/src/main/res/navigation/direct_messages_nav_graph.xml index 1cf39e25..51dfd27b 100644 --- a/app/src/main/res/navigation/direct_messages_nav_graph.xml +++ b/app/src/main/res/navigation/direct_messages_nav_graph.xml @@ -96,6 +96,10 @@ android:id="@+id/action_global_user_search" app:destination="@id/user_search_nav_graph" /> + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/discover_nav_graph.xml b/app/src/main/res/navigation/discover_nav_graph.xml index 537e5eae..2c2d0f08 100644 --- a/app/src/main/res/navigation/discover_nav_graph.xml +++ b/app/src/main/res/navigation/discover_nav_graph.xml @@ -95,6 +95,10 @@ app:argType="long" /> + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/favorites_nav_graph.xml b/app/src/main/res/navigation/favorites_nav_graph.xml index 1a288bf1..a1a218ff 100644 --- a/app/src/main/res/navigation/favorites_nav_graph.xml +++ b/app/src/main/res/navigation/favorites_nav_graph.xml @@ -1,6 +1,7 @@ @@ -36,8 +37,18 @@ app:argType="long" /> + + + android:label="@string/title_favorites" + tools:layout="@layout/fragment_favorites" /> + \ No newline at end of file diff --git a/app/src/main/res/navigation/feed_nav_graph.xml b/app/src/main/res/navigation/feed_nav_graph.xml index f9e26cc8..19dec632 100644 --- a/app/src/main/res/navigation/feed_nav_graph.xml +++ b/app/src/main/res/navigation/feed_nav_graph.xml @@ -106,6 +106,10 @@ app:nullable="false" /> + + - + \ No newline at end of file diff --git a/app/src/main/res/navigation/hashtag_nav_graph.xml b/app/src/main/res/navigation/hashtag_nav_graph.xml index b4d6d31a..7446c366 100644 --- a/app/src/main/res/navigation/hashtag_nav_graph.xml +++ b/app/src/main/res/navigation/hashtag_nav_graph.xml @@ -65,6 +65,10 @@ app:argType="long" /> + + + diff --git a/app/src/main/res/navigation/location_nav_graph.xml b/app/src/main/res/navigation/location_nav_graph.xml index acc33413..3761330f 100644 --- a/app/src/main/res/navigation/location_nav_graph.xml +++ b/app/src/main/res/navigation/location_nav_graph.xml @@ -66,6 +66,10 @@ app:nullable="false" /> + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/profile_nav_graph.xml b/app/src/main/res/navigation/profile_nav_graph.xml index a07c4df1..4bc8c444 100644 --- a/app/src/main/res/navigation/profile_nav_graph.xml +++ b/app/src/main/res/navigation/profile_nav_graph.xml @@ -112,6 +112,10 @@ app:argType="boolean" /> + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9cf27407..8530174f 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -493,4 +493,7 @@ If saved, all DM related features will be disabled on next launch Copy caption Copy reply + Top + Recent + Clear diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index f31e985d..c0d79025 100755 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -225,4 +225,13 @@ 0dp 0dp + + + + From 548582a6d2780a1918a603cb22e8d729d4caca14 Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Wed, 14 Apr 2021 01:02:42 +0900 Subject: [PATCH 010/320] Fix options menu item background in dark black theme. Fixes https://github.com/austinhuang0131/barinsta/issues/1076 --- app/src/main/res/values/themes.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index a0e7a3b8..28de10d8 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -126,7 +126,7 @@ @style/Widget.Dialog.Dark.Black @style/Widget.AlertDialog.Dark.Black @style/Widget.AppCompat.CompoundButton.Switch.Dark.Black - @style/Widget.AppCompat.ListView.DropDown.Dark.Black + @style/PreferenceFragmentCompatStyle.Dark.Black @style/Widget.MaterialComponents.AppBarLayout.Primary @color/grey_600 From ea2956f3caa3080a3c3cc8385dea775bd8165627 Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Wed, 14 Apr 2021 01:08:08 +0900 Subject: [PATCH 011/320] Add state saved check before show. Fixes https://github.com/austinhuang0131/barinsta/issues/1071. --- .../webservices/interceptors/IgErrorsInterceptor.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/awais/instagrabber/webservices/interceptors/IgErrorsInterceptor.java b/app/src/main/java/awais/instagrabber/webservices/interceptors/IgErrorsInterceptor.java index e27eeae2..01cec822 100644 --- a/app/src/main/java/awais/instagrabber/webservices/interceptors/IgErrorsInterceptor.java +++ b/app/src/main/java/awais/instagrabber/webservices/interceptors/IgErrorsInterceptor.java @@ -5,6 +5,7 @@ import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.StringRes; +import androidx.fragment.app.FragmentManager; import com.google.android.material.snackbar.Snackbar; @@ -59,7 +60,7 @@ public class IgErrorsInterceptor implements Interceptor { return; case 302: // redirect final String location = response.header("location"); - if (location.equals("https://www.instagram.com/accounts/login/")) { + if (location != null && location.equals("https://www.instagram.com/accounts/login/")) { // rate limited showErrorDialog(R.string.rate_limit); } @@ -70,7 +71,7 @@ public class IgErrorsInterceptor implements Interceptor { try { final String bodyString = body.string(); final JSONObject jsonObject = new JSONObject(bodyString); - String message = jsonObject.optString("message", null); + String message = jsonObject.optString("message"); if (!TextUtils.isEmpty(message)) { message = message.toLowerCase(); switch (message) { @@ -91,7 +92,7 @@ public class IgErrorsInterceptor implements Interceptor { return; } } - final String errorType = jsonObject.optString("error_type", null); + final String errorType = jsonObject.optString("error_type"); if (TextUtils.isEmpty(errorType)) return; if (errorType.equals("sentry_block")) { showErrorDialog(R.string.sentry_block); @@ -127,7 +128,9 @@ public class IgErrorsInterceptor implements Interceptor { 0, 0 ); - dialogFragment.show(mainActivity.getSupportFragmentManager(), "network_error_dialog"); + final FragmentManager fragmentManager = mainActivity.getSupportFragmentManager(); + if (fragmentManager.isStateSaved()) return; + dialogFragment.show(fragmentManager, "network_error_dialog"); } public void destroy() { From af3670e3ece32aa1af2848534a1bc5c01dd2a835 Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Wed, 14 Apr 2021 01:16:32 +0900 Subject: [PATCH 012/320] Add fragment manager destroyed checks. Fixes https://github.com/austinhuang0131/barinsta/issues/1068 --- .../instagrabber/asyncs/PostFetcher.java | 9 +-- .../fragments/HashTagFragment.java | 13 ++-- .../fragments/LocationFragment.java | 65 ++++++++++--------- 3 files changed, 47 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/awais/instagrabber/asyncs/PostFetcher.java b/app/src/main/java/awais/instagrabber/asyncs/PostFetcher.java index ddd670f1..c0e63ea6 100755 --- a/app/src/main/java/awais/instagrabber/asyncs/PostFetcher.java +++ b/app/src/main/java/awais/instagrabber/asyncs/PostFetcher.java @@ -12,9 +12,6 @@ import awais.instagrabber.interfaces.FetchListener; import awais.instagrabber.repositories.responses.Media; import awais.instagrabber.utils.NetworkUtils; import awais.instagrabber.utils.ResponseBodyUtils; -//import awaisomereport.LogCollector; - -//import static awais.instagrabber.utils.Utils.logCollector; public final class PostFetcher extends AsyncTask { private static final String TAG = "PostFetcher"; @@ -136,9 +133,9 @@ public final class PostFetcher extends AsyncTask { return ResponseBodyUtils.parseGraphQLItem(media, null); } } catch (Exception e) { -// if (logCollector != null) { -// logCollector.appendException(e, LogCollector.LogFile.ASYNC_POST_FETCHER, "doInBackground"); -// } + // if (logCollector != null) { + // logCollector.appendException(e, LogCollector.LogFile.ASYNC_POST_FETCHER, "doInBackground"); + // } Log.e(TAG, "Error fetching post", e); } finally { if (conn != null) { diff --git a/app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java b/app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java index 81530dc5..c620f4f2 100644 --- a/app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java @@ -26,6 +26,7 @@ import androidx.appcompat.app.ActionBar; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.content.PermissionChecker; import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; import androidx.navigation.NavController; import androidx.navigation.NavDirections; import androidx.navigation.fragment.NavHostFragment; @@ -60,6 +61,7 @@ import awais.instagrabber.repositories.requests.StoryViewerOptions; import awais.instagrabber.repositories.responses.Hashtag; import awais.instagrabber.repositories.responses.Location; import awais.instagrabber.repositories.responses.Media; +import awais.instagrabber.repositories.responses.User; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.DownloadUtils; @@ -213,7 +215,9 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe final View mainPostImage, final int position) { if (opening) return; - if (TextUtils.isEmpty(feedModel.getUser().getUsername())) { + final User user = feedModel.getUser(); + if (user == null) return; + if (TextUtils.isEmpty(user.getUsername())) { opening = true; new PostFetcher(feedModel.getCode(), newFeedModel -> { opening = false; @@ -231,7 +235,9 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe builder.setSharedProfilePicElement(profilePicView) .setSharedMainPostElement(mainPostImage); } - builder.build().show(getChildFragmentManager(), "post_view"); + final FragmentManager fragmentManager = getChildFragmentManager(); + if (fragmentManager.isDestroyed()) return; + builder.build().show(fragmentManager, "post_view"); opening = false; } }; @@ -403,8 +409,7 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe try { Toast.makeText(getContext(), R.string.error_loading_hashtag, Toast.LENGTH_SHORT).show(); binding.swipeRefreshLayout.setEnabled(false); - } - catch (Exception ignored) {} + } catch (Exception ignored) {} return; } setTitle(); diff --git a/app/src/main/java/awais/instagrabber/fragments/LocationFragment.java b/app/src/main/java/awais/instagrabber/fragments/LocationFragment.java index c42809c5..75690dd0 100644 --- a/app/src/main/java/awais/instagrabber/fragments/LocationFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/LocationFragment.java @@ -24,6 +24,7 @@ import androidx.appcompat.app.ActionBar; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.content.PermissionChecker; import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; import androidx.navigation.NavController; import androidx.navigation.NavDirections; import androidx.navigation.fragment.NavHostFragment; @@ -56,6 +57,7 @@ import awais.instagrabber.models.enums.FavoriteType; import awais.instagrabber.repositories.requests.StoryViewerOptions; import awais.instagrabber.repositories.responses.Location; import awais.instagrabber.repositories.responses.Media; +import awais.instagrabber.repositories.responses.User; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.DownloadUtils; @@ -204,7 +206,9 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR final View mainPostImage, final int position) { if (opening) return; - if (TextUtils.isEmpty(feedModel.getUser().getUsername())) { + final User user = feedModel.getUser(); + if (user == null) return; + if (TextUtils.isEmpty(user.getUsername())) { opening = true; new PostFetcher(feedModel.getCode(), newFeedModel -> { opening = false; @@ -223,7 +227,9 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR builder.setSharedProfilePicElement(profilePicView) .setSharedMainPostElement(mainPostImage); } - builder.build().show(getChildFragmentManager(), "post_view"); + final FragmentManager fragmentManager = getChildFragmentManager(); + if (fragmentManager.isDestroyed()) return; + builder.build().show(fragmentManager, "post_view"); opening = false; } }; @@ -399,8 +405,7 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR try { Toast.makeText(getContext(), R.string.error_loading_location, Toast.LENGTH_SHORT).show(); binding.swipeRefreshLayout.setEnabled(false); - } - catch (Exception ignored) {} + } catch (Exception ignored) {} return; } setTitle(); @@ -409,16 +414,16 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR final long locationId = locationModel.getPk(); // binding.swipeRefreshLayout.setRefreshing(true); locationDetailsBinding.mainLocationImage.setImageURI("res:/" + R.drawable.ic_location); -// final String postCount = String.valueOf(locationModel.getCount()); -// final SpannableStringBuilder span = new SpannableStringBuilder(getResources().getQuantityString(R.plurals.main_posts_count_inline, -// locationModel.getPostCount() > 2000000000L -// ? 2000000000 -// : locationModel.getPostCount().intValue(), -// postCount)); -// span.setSpan(new RelativeSizeSpan(1.2f), 0, postCount.length(), 0); -// span.setSpan(new StyleSpan(Typeface.BOLD), 0, postCount.length(), 0); -// locationDetailsBinding.mainLocPostCount.setText(span); -// locationDetailsBinding.mainLocPostCount.setVisibility(View.VISIBLE); + // final String postCount = String.valueOf(locationModel.getCount()); + // final SpannableStringBuilder span = new SpannableStringBuilder(getResources().getQuantityString(R.plurals.main_posts_count_inline, + // locationModel.getPostCount() > 2000000000L + // ? 2000000000 + // : locationModel.getPostCount().intValue(), + // postCount)); + // span.setSpan(new RelativeSizeSpan(1.2f), 0, postCount.length(), 0); + // span.setSpan(new StyleSpan(Typeface.BOLD), 0, postCount.length(), 0); + // locationDetailsBinding.mainLocPostCount.setText(span); + // locationDetailsBinding.mainLocPostCount.setVisibility(View.VISIBLE); locationDetailsBinding.locationFullName.setText(locationModel.getName()); CharSequence biography = locationModel.getAddress() + "\n" + locationModel.getCity(); // binding.locationBiography.setCaptionIsExpandable(true); @@ -431,22 +436,22 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR } else { locationDetailsBinding.locationBiography.setVisibility(View.VISIBLE); locationDetailsBinding.locationBiography.setText(biography); -// locationDetailsBinding.locationBiography.addOnHashtagListener(autoLinkItem -> { -// final NavController navController = NavHostFragment.findNavController(this); -// final Bundle bundle = new Bundle(); -// final String originalText = autoLinkItem.getOriginalText().trim(); -// bundle.putString(ARG_HASHTAG, originalText); -// navController.navigate(R.id.action_global_hashTagFragment, bundle); -// }); -// locationDetailsBinding.locationBiography.addOnMentionClickListener(autoLinkItem -> { -// final String originalText = autoLinkItem.getOriginalText().trim(); -// navigateToProfile(originalText); -// }); -// locationDetailsBinding.locationBiography.addOnEmailClickListener(autoLinkItem -> Utils.openEmailAddress(context, -// autoLinkItem.getOriginalText() -// .trim())); -// locationDetailsBinding.locationBiography -// .addOnURLClickListener(autoLinkItem -> Utils.openURL(context, autoLinkItem.getOriginalText().trim())); + // locationDetailsBinding.locationBiography.addOnHashtagListener(autoLinkItem -> { + // final NavController navController = NavHostFragment.findNavController(this); + // final Bundle bundle = new Bundle(); + // final String originalText = autoLinkItem.getOriginalText().trim(); + // bundle.putString(ARG_HASHTAG, originalText); + // navController.navigate(R.id.action_global_hashTagFragment, bundle); + // }); + // locationDetailsBinding.locationBiography.addOnMentionClickListener(autoLinkItem -> { + // final String originalText = autoLinkItem.getOriginalText().trim(); + // navigateToProfile(originalText); + // }); + // locationDetailsBinding.locationBiography.addOnEmailClickListener(autoLinkItem -> Utils.openEmailAddress(context, + // autoLinkItem.getOriginalText() + // .trim())); + // locationDetailsBinding.locationBiography + // .addOnURLClickListener(autoLinkItem -> Utils.openURL(context, autoLinkItem.getOriginalText().trim())); locationDetailsBinding.locationBiography.setOnLongClickListener(v -> { Utils.copyText(context, biography); return true; From de9891f368dfecf98122b6de55ed0430ee97fede Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Wed, 14 Apr 2021 01:27:31 +0900 Subject: [PATCH 013/320] Add null check. Fixes https://github.com/austinhuang0131/barinsta/issues/1078 --- .../customviews/VideoPlayerViewHelper.java | 50 ++++++++++--------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/awais/instagrabber/customviews/VideoPlayerViewHelper.java b/app/src/main/java/awais/instagrabber/customviews/VideoPlayerViewHelper.java index b2cfc2f1..b8776bfd 100644 --- a/app/src/main/java/awais/instagrabber/customviews/VideoPlayerViewHelper.java +++ b/app/src/main/java/awais/instagrabber/customviews/VideoPlayerViewHelper.java @@ -14,8 +14,8 @@ import androidx.appcompat.widget.AppCompatTextView; import androidx.appcompat.widget.PopupMenu; import com.facebook.drawee.backends.pipeline.Fresco; +import com.facebook.drawee.backends.pipeline.PipelineDraweeControllerBuilder; import com.facebook.drawee.controller.BaseControllerListener; -import com.facebook.drawee.interfaces.DraweeController; import com.facebook.imagepipeline.image.ImageInfo; import com.facebook.imagepipeline.request.ImageRequest; import com.facebook.imagepipeline.request.ImageRequestBuilder; @@ -156,29 +156,33 @@ public class VideoPlayerViewHelper implements Player.EventListener { private void setThumbnail() { binding.thumbnail.setAspectRatio(thumbnailAspectRatio); - final ImageRequest thumbnailRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(thumbnailUrl)) - .build(); - final DraweeController controller = Fresco.newDraweeControllerBuilder() - .setControllerListener(new BaseControllerListener() { - @Override - public void onFailure(final String id, final Throwable throwable) { - if (videoPlayerCallback != null) { - videoPlayerCallback.onThumbnailLoaded(); - } - } + ImageRequest thumbnailRequest = null; + if (thumbnailUrl != null) { + thumbnailRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(thumbnailUrl)).build(); + } + final PipelineDraweeControllerBuilder builder = Fresco.newDraweeControllerBuilder() + .setControllerListener(new BaseControllerListener() { + @Override + public void onFailure(final String id, + final Throwable throwable) { + if (videoPlayerCallback != null) { + videoPlayerCallback.onThumbnailLoaded(); + } + } - @Override - public void onFinalImageSet(final String id, - final ImageInfo imageInfo, - final Animatable animatable) { - if (videoPlayerCallback != null) { - videoPlayerCallback.onThumbnailLoaded(); - } - } - }) - .setImageRequest(thumbnailRequest) - .build(); - binding.thumbnail.setController(controller); + @Override + public void onFinalImageSet(final String id, + final ImageInfo imageInfo, + final Animatable animatable) { + if (videoPlayerCallback != null) { + videoPlayerCallback.onThumbnailLoaded(); + } + } + }); + if (thumbnailRequest != null) { + builder.setImageRequest(thumbnailRequest); + } + binding.thumbnail.setController(builder.build()); } private void loadPlayer() { From 34e54837d5eccce0f961c2e7818ced7aaf87ec43 Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Wed, 14 Apr 2021 20:17:00 +0900 Subject: [PATCH 014/320] Create new caption object if null. Fixes https://github.com/austinhuang0131/barinsta/issues/1030 --- .../instagrabber/repositories/responses/Media.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/Media.java b/app/src/main/java/awais/instagrabber/repositories/responses/Media.java index c11669a8..135eee89 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/Media.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/Media.java @@ -34,7 +34,7 @@ public class Media implements Serializable { private final boolean hasAudio; private final double videoDuration; private final long viewCount; - private final Caption caption; + private Caption caption; private final boolean canViewerSave; private final Audio audio; private final String title; @@ -271,7 +271,14 @@ public class Media implements Serializable { } public void setPostCaption(final String caption) { - final Caption caption1 = getCaption(); + Caption caption1 = getCaption(); + if (caption1 == null) { + final User user = getUser(); + if (user == null) return; + caption1 = new Caption(user.getPk(), caption); + this.caption = caption1; + return; + } caption1.setText(caption); } From d4ee1b9f4217d33cc787697fc5c3435f6ac7946f Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Wed, 14 Apr 2021 20:23:42 +0900 Subject: [PATCH 015/320] Set click listener on parent view. Fixes https://github.com/austinhuang0131/barinsta/issues/1055 --- .../adapters/viewholder/feed/FeedItemViewHolder.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedItemViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedItemViewHolder.java index 860bd027..042264c9 100644 --- a/app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedItemViewHolder.java +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedItemViewHolder.java @@ -55,7 +55,7 @@ public abstract class FeedItemViewHolder extends RecyclerView.ViewHolder { private void setupComments(@NonNull final Media feedModel) { final long commentsCount = feedModel.getCommentCount(); bottomBinding.commentsCount.setText(String.valueOf(commentsCount)); - bottomBinding.commentsCount.setOnClickListener(v -> feedItemCallback.onCommentsClick(feedModel)); + bottomBinding.btnComments.setOnClickListener(v -> feedItemCallback.onCommentsClick(feedModel)); } private void setupProfilePic(@NonNull final Media media) { @@ -75,6 +75,7 @@ public abstract class FeedItemViewHolder extends RecyclerView.ViewHolder { // final SpannableString spannableString = new SpannableString(); // spannableString.setSpan(new CommentMentionClickSpan(), 0, titleLen, 0); final User user = media.getUser(); + if (user == null) return; final String title = "@" + user.getUsername(); topBinding.title.setText(title); topBinding.title.setOnClickListener(v -> feedItemCallback.onNameClick(media, topBinding.ivProfilePic)); @@ -120,8 +121,7 @@ public abstract class FeedItemViewHolder extends RecyclerView.ViewHolder { topBinding.title.setLayoutParams(new RelativeLayout.LayoutParams( RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT )); - } - else { + } else { final String locationName = location.getName(); if (TextUtils.isEmpty(locationName)) { topBinding.location.setVisibility(View.GONE); From f2a3506b19a0f05277ea5dc41d78767fcc74168a Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Wed, 14 Apr 2021 20:48:42 +0900 Subject: [PATCH 016/320] Make refreshStory synchronised? May fix https://github.com/austinhuang0131/barinsta/issues/945 --- .../fragments/StoryViewerFragment.java | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java b/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java index ddd84ce3..c13efdc1 100644 --- a/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java @@ -2,14 +2,12 @@ package awais.instagrabber.fragments; import android.annotation.SuppressLint; import android.content.Context; -import android.content.Intent; import android.content.pm.PackageManager; import android.graphics.drawable.Animatable; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.util.Log; -import android.util.Pair; import android.view.GestureDetector; import android.view.Gravity; import android.view.LayoutInflater; @@ -72,7 +70,6 @@ import awais.instagrabber.BuildConfig; import awais.instagrabber.R; import awais.instagrabber.adapters.StoriesAdapter; import awais.instagrabber.asyncs.CreateThreadAction; -import awais.instagrabber.asyncs.PostFetcher; import awais.instagrabber.customviews.helpers.SwipeGestureListener; import awais.instagrabber.databinding.FragmentStoryViewerBinding; import awais.instagrabber.fragments.main.ProfileFragmentDirections; @@ -105,7 +102,6 @@ import awais.instagrabber.webservices.DirectMessagesService; import awais.instagrabber.webservices.MediaService; import awais.instagrabber.webservices.ServiceCallback; import awais.instagrabber.webservices.StoriesService; -//import awaisomereport.LogCollector; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; @@ -113,7 +109,6 @@ import retrofit2.Response; import static awais.instagrabber.customviews.helpers.SwipeGestureListener.SWIPE_THRESHOLD; import static awais.instagrabber.customviews.helpers.SwipeGestureListener.SWIPE_VELOCITY_THRESHOLD; import static awais.instagrabber.utils.Constants.MARK_AS_SEEN; -//import static awais.instagrabber.utils.Utils.logCollector; import static awais.instagrabber.utils.Utils.settingsHelper; public class StoryViewerFragment extends Fragment { @@ -417,10 +412,10 @@ public class StoryViewerFragment extends Fragment { return true; } } catch (final Exception e) { -// if (logCollector != null) -// logCollector.appendException(e, LogCollector.LogFile.ACTIVITY_STORY_VIEWER, "setupListeners", -// new Pair<>("swipeEvent", swipeEvent), -// new Pair<>("diffX", diffX)); + // if (logCollector != null) + // logCollector.appendException(e, LogCollector.LogFile.ACTIVITY_STORY_VIEWER, "setupListeners", + // new Pair<>("swipeEvent", swipeEvent), + // new Pair<>("diffX", diffX)); if (BuildConfig.DEBUG) Log.e(TAG, "Error", e); } return false; @@ -838,7 +833,7 @@ public class StoryViewerFragment extends Fragment { } } - private void refreshStory() { + private synchronized void refreshStory() { if (binding.storiesList.getVisibility() == View.VISIBLE) { final List storyModels = storiesViewModel.getList().getValue(); if (storyModels != null && storyModels.size() > 0) { From eed4036ad94ff29636cabd39fc888e8618a60d9c Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Wed, 14 Apr 2021 21:17:07 +0900 Subject: [PATCH 017/320] Fix setItemsToThread. Fixes https://github.com/austinhuang0131/barinsta/issues/967 --- .../java/awais/instagrabber/managers/InboxManager.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/awais/instagrabber/managers/InboxManager.java b/app/src/main/java/awais/instagrabber/managers/InboxManager.java index efca55f5..eb54b8e2 100644 --- a/app/src/main/java/awais/instagrabber/managers/InboxManager.java +++ b/app/src/main/java/awais/instagrabber/managers/InboxManager.java @@ -285,8 +285,13 @@ public final class InboxManager { if (index < 0) return; final List threads = inbox.getThreads(); final DirectThread thread = threads.get(index); - thread.setItems(updatedItems); - setThread(inbox, index, thread); + try { + final DirectThread threadClone = (DirectThread) thread.clone(); + threadClone.setItems(updatedItems); + setThread(inbox, index, threadClone); + } catch (Exception e) { + Log.e(TAG, "setItemsToThread: ", e); + } } } From cfd1d4a421685791ff8597343b5e33c7f279d9a2 Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Wed, 14 Apr 2021 21:17:24 +0900 Subject: [PATCH 018/320] Null check csrf token --- .../java/awais/instagrabber/managers/ThreadManager.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/awais/instagrabber/managers/ThreadManager.java b/app/src/main/java/awais/instagrabber/managers/ThreadManager.java index 5b84c97c..6074363e 100644 --- a/app/src/main/java/awais/instagrabber/managers/ThreadManager.java +++ b/app/src/main/java/awais/instagrabber/managers/ThreadManager.java @@ -92,14 +92,14 @@ public final class ThreadManager { private final MutableLiveData pendingRequests = new MutableLiveData<>(null); private final String threadId; - private final DirectMessagesService service; private final long viewerId; private final ThreadIdOrUserIds threadIdOrUserIds; private final User currentUser; private final ContentResolver contentResolver; - private final MediaService mediaService; - private final FriendshipService friendshipService; + private DirectMessagesService service; + private MediaService mediaService; + private FriendshipService friendshipService; private InboxManager inboxManager; private LiveData thread; private LiveData inputMode; @@ -157,6 +157,7 @@ public final class ThreadManager { viewerId = CookieUtils.getUserIdFromCookie(cookie); final String deviceUuid = settingsHelper.getString(Constants.DEVICE_UUID); final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); + if (csrfToken == null) return; // if (TextUtils.isEmpty(csrfToken) || viewerId <= 0 || TextUtils.isEmpty(deviceUuid)) { // throw new IllegalArgumentException("User is not logged in!"); // } From 81a55151c18ec7d95c29a2ac9927b9a59e484fc9 Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Thu, 15 Apr 2021 23:33:00 +0900 Subject: [PATCH 019/320] Fix styles --- app/src/main/res/layout/activity_main.xml | 4 ++-- app/src/main/res/values/attrs.xml | 3 +++ app/src/main/res/values/styles.xml | 26 +++++++++++++++++++++++ app/src/main/res/values/themes.xml | 15 +++++++++++++ 4 files changed, 46 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 8fadd4a2..1e46b341 100755 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -29,14 +29,14 @@ android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" - android:background="?attr/colorSurface" + android:background="?attr/toolbarColor" app:layout_collapseMode="pin" app:title="@string/app_name" tools:menu="@menu/main_menu"> + + + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index c0d79025..129f3875 100755 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -234,4 +234,30 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 28de10d8..cceca619 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -23,6 +23,7 @@ @color/grey_600 @color/deep_purple_400 @color/deep_purple_600 + @style/Widget.MaterialComponents.TextInputLayout.OutlinedBox @@ -103,6 +112,7 @@ @color/white @color/blue_800 @color/white + @style/Widget.MaterialComponents.TextInputLayout.OutlinedBox + + + + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index cceca619..d781845f 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -146,6 +146,7 @@ @color/deep_purple_400 @color/deep_purple_600 @style/Widget.MaterialComponents.TabLayout.Dark.Black + @style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dark.Black - - diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index d781845f..660a2189 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -53,6 +53,7 @@ @color/blue_800 @color/black @style/Widget.MaterialComponents.TabLayout.Light.White + @color/parent_comment_light_white @@ -146,7 +149,8 @@ @color/deep_purple_400 @color/deep_purple_600 @style/Widget.MaterialComponents.TabLayout.Dark.Black - @style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dark.Black + @style/Widget.MaterialComponents.TextInputLayout.FilledBox.Dark.Black + @color/parent_comment_dark_materialdark