315 changed files with 10999 additions and 5582 deletions
-
46.all-contributorsrc
-
5.codebeatsettings
-
2.idea/compiler.xml
-
1.idea/gradle.xml
-
2.idea/misc.xml
-
1.idea/runConfigurations.xml
-
4.project
-
29README.md
-
40app/build.gradle
-
227app/schemas/awais.instagrabber.db.AppDatabase/6.json
-
51app/src/androidTest/java/awais/instagrabber/db/MigrationTest.java
-
82app/src/androidTest/java/awais/instagrabber/db/dao/RecentSearchDaoTest.java
-
6app/src/github/res/values-ca/strings.xml
-
6app/src/github/res/values-cs/strings.xml
-
6app/src/github/res/values-de/strings.xml
-
6app/src/github/res/values-el/strings.xml
-
6app/src/github/res/values-es/strings.xml
-
6app/src/github/res/values-eu/strings.xml
-
6app/src/github/res/values-fa/strings.xml
-
6app/src/github/res/values-fr/strings.xml
-
6app/src/github/res/values-hi/strings.xml
-
6app/src/github/res/values-in/strings.xml
-
6app/src/github/res/values-it/strings.xml
-
6app/src/github/res/values-ja/strings.xml
-
6app/src/github/res/values-mk/strings.xml
-
6app/src/github/res/values-nl/strings.xml
-
6app/src/github/res/values-or/strings.xml
-
6app/src/github/res/values-pl/strings.xml
-
6app/src/github/res/values-pt/strings.xml
-
6app/src/github/res/values-ru/strings.xml
-
6app/src/github/res/values-sk/strings.xml
-
6app/src/github/res/values-sv/strings.xml
-
6app/src/github/res/values-tr/strings.xml
-
6app/src/github/res/values-vi/strings.xml
-
6app/src/github/res/values-zh-rCN/strings.xml
-
6app/src/github/res/values-zh-rTW/strings.xml
-
7app/src/main/AndroidManifest.xml
-
5app/src/main/java/awais/instagrabber/InstaGrabberApplication.java
-
24app/src/main/java/awais/instagrabber/activities/Login.java
-
425app/src/main/java/awais/instagrabber/activities/MainActivity.java
-
197app/src/main/java/awais/instagrabber/adapters/CommentsAdapter.java
-
2app/src/main/java/awais/instagrabber/adapters/DirectItemsAdapter.java
-
4app/src/main/java/awais/instagrabber/adapters/FavoritesAdapter.java
-
2app/src/main/java/awais/instagrabber/adapters/LikesAdapter.java
-
33app/src/main/java/awais/instagrabber/adapters/SearchCategoryAdapter.java
-
215app/src/main/java/awais/instagrabber/adapters/SearchItemsAdapter.java
-
16app/src/main/java/awais/instagrabber/adapters/SliderCallbackAdapter.java
-
23app/src/main/java/awais/instagrabber/adapters/SliderItemsAdapter.java
-
77app/src/main/java/awais/instagrabber/adapters/SuggestionsAdapter.java
-
209app/src/main/java/awais/instagrabber/adapters/viewholder/CommentViewHolder.java
-
20app/src/main/java/awais/instagrabber/adapters/viewholder/FavoriteViewHolder.java
-
21app/src/main/java/awais/instagrabber/adapters/viewholder/FollowsViewHolder.java
-
80app/src/main/java/awais/instagrabber/adapters/viewholder/SearchItemViewHolder.java
-
77app/src/main/java/awais/instagrabber/adapters/viewholder/SliderPhotoViewHolder.java
-
118app/src/main/java/awais/instagrabber/adapters/viewholder/SliderVideoViewHolder.java
-
95app/src/main/java/awais/instagrabber/adapters/viewholder/comments/ChildCommentViewHolder.java
-
95app/src/main/java/awais/instagrabber/adapters/viewholder/comments/ParentCommentViewHolder.java
-
8app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemAnimatedMediaViewHolder.java
-
8app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemMediaShareViewHolder.java
-
9app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemMediaViewHolder.java
-
8app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemRavenMediaViewHolder.java
-
9app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemStoryShareViewHolder.java
-
9app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemViewHolder.java
-
8app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemXmaViewHolder.java
-
6app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedItemViewHolder.java
-
4app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedSliderViewHolder.java
-
2app/src/main/java/awais/instagrabber/adapters/viewholder/feed/FeedVideoViewHolder.java
-
268app/src/main/java/awais/instagrabber/asyncs/CommentsFetcher.java
-
9app/src/main/java/awais/instagrabber/asyncs/PostFetcher.java
-
165app/src/main/java/awais/instagrabber/customviews/FormattedNumberTextView.java
-
75app/src/main/java/awais/instagrabber/customviews/FragmentNavigatorWithDefaultAnimations.java
-
60app/src/main/java/awais/instagrabber/customviews/NavHostFragmentWithDefaultAnimations.java
-
36app/src/main/java/awais/instagrabber/customviews/PostsRecyclerView.java
-
6app/src/main/java/awais/instagrabber/customviews/ProfilePicView.java
-
25app/src/main/java/awais/instagrabber/customviews/RamboTextViewV2.java
-
95app/src/main/java/awais/instagrabber/customviews/TextViewDrawableSize.java
-
7app/src/main/java/awais/instagrabber/customviews/Tooltip.java
-
77app/src/main/java/awais/instagrabber/customviews/UsernameTextView.java
-
10app/src/main/java/awais/instagrabber/customviews/VideoPlayerCallbackAdapter.java
-
446app/src/main/java/awais/instagrabber/customviews/VideoPlayerViewHelper.java
-
2app/src/main/java/awais/instagrabber/customviews/drawee/ZoomableDraweeView.java
-
100app/src/main/java/awais/instagrabber/customviews/emoji/EmojiBottomSheetDialog.java
-
31app/src/main/java/awais/instagrabber/customviews/emoji/EmojiGridAdapter.java
-
163app/src/main/java/awais/instagrabber/customviews/emoji/EmojiPopupWindow.java
-
1app/src/main/java/awais/instagrabber/customviews/emoji/GoogleCompatEmojiDrawable.java
-
320app/src/main/java/awais/instagrabber/customviews/helpers/ChangeText.java
-
17app/src/main/java/awais/instagrabber/customviews/helpers/CustomHideBottomViewOnScrollBehavior.java
-
19app/src/main/java/awais/instagrabber/customviews/helpers/GridSpacingItemDecoration.java
-
25app/src/main/java/awais/instagrabber/db/AppDatabase.java
-
37app/src/main/java/awais/instagrabber/db/dao/RecentSearchDao.java
-
57app/src/main/java/awais/instagrabber/db/datasources/RecentSearchDataSource.java
-
185app/src/main/java/awais/instagrabber/db/entities/RecentSearch.java
-
124app/src/main/java/awais/instagrabber/db/repositories/RecentSearchRepository.java
-
14app/src/main/java/awais/instagrabber/dialogs/AccountSwitcherDialogFragment.java
-
116app/src/main/java/awais/instagrabber/fragments/CollectionPostsFragment.java
-
487app/src/main/java/awais/instagrabber/fragments/CommentsViewerFragment.java
-
4app/src/main/java/awais/instagrabber/fragments/FavoritesFragment.java
-
129app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java
-
11app/src/main/java/awais/instagrabber/fragments/LikesViewerFragment.java
-
123app/src/main/java/awais/instagrabber/fragments/LocationFragment.java
@ -0,0 +1,5 @@ |
|||
{ |
|||
"JAVA": { |
|||
"TOO_MANY_IVARS": [8, 10, 20, 30] |
|||
} |
|||
} |
@ -1,6 +1,6 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<project version="4"> |
|||
<component name="CompilerConfiguration"> |
|||
<bytecodeTargetLevel target="1.8" /> |
|||
<bytecodeTargetLevel target="11" /> |
|||
</component> |
|||
</project> |
@ -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')" |
|||
] |
|||
} |
|||
} |
@ -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(); |
|||
} |
|||
} |
@ -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<RecentSearch> insertListReversed = ImmutableList |
|||
.<RecentSearch>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<RecentSearch> 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; |
|||
} |
|||
} |
@ -0,0 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<string name="enable_sentry">Habilita el Sentry</string> |
|||
<string name="sentry_summary">Sentry és un oient/intèrpret d\'error que envia asíncronament l\'error/esdeveniment a Sentry.io</string> |
|||
<string name="sentry_start_next_launch">Sentry s\'iniciarà al pròxim llançament</string> |
|||
</resources> |
@ -0,0 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<string name="enable_sentry">Povolit Sentry</string> |
|||
<string name="sentry_summary">Sentry je listener/handler, který zaznamenává chyby a asynchronně je posílá na Sentry.io</string> |
|||
<string name="sentry_start_next_launch">Sentry se spustí při příštím spuštění</string> |
|||
</resources> |
@ -0,0 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<string name="enable_sentry">Enable Sentry</string> |
|||
<string name="sentry_summary">Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io</string> |
|||
<string name="sentry_start_next_launch">Sentry will start on next launch</string> |
|||
</resources> |
@ -0,0 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<string name="enable_sentry">Ενεργοποίηση Sentry</string> |
|||
<string name="sentry_summary">Το Sentry είναι διαχειριστής σφαλμάτων ασύγχρονης αποστολής του σφάλματος/συμβάντος στο Sentry.io</string> |
|||
<string name="sentry_start_next_launch">Το Sentry θα ξεκινήσει στην επόμενη εκκίνηση</string> |
|||
</resources> |
@ -0,0 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<string name="enable_sentry">Activar Sentry</string> |
|||
<string name="sentry_summary">Sentry es un oyente/manejador de errores que asincrónicamente envía el error/evento a Sentry.io</string> |
|||
<string name="sentry_start_next_launch">Sentry comenzará en el próximo inicio</string> |
|||
</resources> |
@ -0,0 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<string name="enable_sentry">Enable Sentry</string> |
|||
<string name="sentry_summary">Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io</string> |
|||
<string name="sentry_start_next_launch">Sentry will start on next launch</string> |
|||
</resources> |
@ -0,0 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<string name="enable_sentry">Enable Sentry</string> |
|||
<string name="sentry_summary">Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io</string> |
|||
<string name="sentry_start_next_launch">Sentry will start on next launch</string> |
|||
</resources> |
@ -0,0 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<string name="enable_sentry">Activer Sentry</string> |
|||
<string name="sentry_summary">Sentry est un écouteur/gestionnaire d\'erreurs qui envoie de manière asynchrone l\'erreur/l\'événement à Sentry.io</string> |
|||
<string name="sentry_start_next_launch">Sentry commencera au prochain lancement</string> |
|||
</resources> |
@ -0,0 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<string name="enable_sentry">Enable Sentry</string> |
|||
<string name="sentry_summary">Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io</string> |
|||
<string name="sentry_start_next_launch">Sentry will start on next launch</string> |
|||
</resources> |
@ -0,0 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<string name="enable_sentry">Enable Sentry</string> |
|||
<string name="sentry_summary">Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io</string> |
|||
<string name="sentry_start_next_launch">Sentry will start on next launch</string> |
|||
</resources> |
@ -0,0 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<string name="enable_sentry">Abilita Sentry</string> |
|||
<string name="sentry_summary">Sentry è un ascoltatore/gestore di errori che invia asincronicamente l\'errore/evento a Sentry.io</string> |
|||
<string name="sentry_start_next_launch">Sentry comincerà al prossimo lancio</string> |
|||
</resources> |
@ -0,0 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<string name="enable_sentry">Enable Sentry</string> |
|||
<string name="sentry_summary">Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io</string> |
|||
<string name="sentry_start_next_launch">Sentry will start on next launch</string> |
|||
</resources> |
@ -0,0 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<string name="enable_sentry">Enable Sentry</string> |
|||
<string name="sentry_summary">Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io</string> |
|||
<string name="sentry_start_next_launch">Sentry will start on next launch</string> |
|||
</resources> |
@ -0,0 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<string name="enable_sentry">Enable Sentry</string> |
|||
<string name="sentry_summary">Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io</string> |
|||
<string name="sentry_start_next_launch">Sentry will start on next launch</string> |
|||
</resources> |
@ -0,0 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<string name="enable_sentry">Enable Sentry</string> |
|||
<string name="sentry_summary">Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io</string> |
|||
<string name="sentry_start_next_launch">Sentry will start on next launch</string> |
|||
</resources> |
@ -0,0 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<string name="enable_sentry">Włącz Sentry</string> |
|||
<string name="sentry_summary">Sentry jest słuchaczem/obsługą błędów, które asynchronicznie wysyłają błąd/zdarzenie do Sentry.io</string> |
|||
<string name="sentry_start_next_launch">Sentry rozpocznie się przy następnym uruchomieniu</string> |
|||
</resources> |
@ -0,0 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<string name="enable_sentry">Ativar Sentry</string> |
|||
<string name="sentry_summary">Sentry é um ouvinte/gestor de erros que assincronicamente envia o erro/evento para Sentry.io</string> |
|||
<string name="sentry_start_next_launch">Sentry começará no próximo início</string> |
|||
</resources> |
@ -0,0 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<string name="enable_sentry">Включить режим \"часового\"</string> |
|||
<string name="sentry_summary">\"Часовой\" - это слушатель/обработчик ошибок, который асинхронно отправляет ошибку/событие на Sentry.io</string> |
|||
<string name="sentry_start_next_launch">\"Часовой\" будет запущен при следующем запуске</string> |
|||
</resources> |
@ -0,0 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<string name="enable_sentry">Enable Sentry</string> |
|||
<string name="sentry_summary">Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io</string> |
|||
<string name="sentry_start_next_launch">Sentry will start on next launch</string> |
|||
</resources> |
@ -0,0 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<string name="enable_sentry">Enable Sentry</string> |
|||
<string name="sentry_summary">Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io</string> |
|||
<string name="sentry_start_next_launch">Sentry will start on next launch</string> |
|||
</resources> |
@ -0,0 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<string name="enable_sentry">Enable Sentry</string> |
|||
<string name="sentry_summary">Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io</string> |
|||
<string name="sentry_start_next_launch">Sentry will start on next launch</string> |
|||
</resources> |
@ -0,0 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<string name="enable_sentry">Enable Sentry</string> |
|||
<string name="sentry_summary">Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io</string> |
|||
<string name="sentry_start_next_launch">Sentry will start on next launch</string> |
|||
</resources> |
@ -0,0 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<string name="enable_sentry">启用 Sentry</string> |
|||
<string name="sentry_summary">Sentry 会将错误报告发送至 Sentry.io</string> |
|||
<string name="sentry_start_next_launch">启用 Sentry 将在下次启动应用时生效</string> |
|||
</resources> |
@ -0,0 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<string name="enable_sentry">啟用 Sentry</string> |
|||
<string name="sentry_summary">Sentry 將會錯誤報告發送至 Sentry.io</string> |
|||
<string name="sentry_start_next_launch">下次啟用應用程式時將會開啟 Sentry</string> |
|||
</resources> |
@ -0,0 +1,33 @@ |
|||
package awais.instagrabber.adapters; |
|||
|
|||
import androidx.annotation.NonNull; |
|||
import androidx.fragment.app.Fragment; |
|||
import androidx.viewpager2.adapter.FragmentStateAdapter; |
|||
|
|||
import java.util.List; |
|||
|
|||
import awais.instagrabber.fragments.search.SearchCategoryFragment; |
|||
import awais.instagrabber.models.enums.FavoriteType; |
|||
|
|||
public class SearchCategoryAdapter extends FragmentStateAdapter { |
|||
|
|||
private final List<FavoriteType> categories; |
|||
|
|||
public SearchCategoryAdapter(@NonNull final Fragment fragment, |
|||
@NonNull final List<FavoriteType> 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(); |
|||
} |
|||
} |
@ -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<RecyclerView.ViewHolder> { |
|||
private static final String TAG = SearchItemsAdapter.class.getSimpleName(); |
|||
private static final DiffUtil.ItemCallback<SearchItemOrHeader> DIFF_CALLBACK = new DiffUtil.ItemCallback<SearchItemOrHeader>() { |
|||
@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<SearchItemOrHeader> 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<SearchItem> list) { |
|||
if (list == null) { |
|||
differ.submitList(null); |
|||
return; |
|||
} |
|||
differ.submitList(sectionAndSort(list)); |
|||
} |
|||
|
|||
public void submitList(@Nullable final List<SearchItem> list, @Nullable final Runnable commitCallback) { |
|||
if (list == null) { |
|||
differ.submitList(null, commitCallback); |
|||
return; |
|||
} |
|||
differ.submitList(sectionAndSort(list), commitCallback); |
|||
} |
|||
|
|||
@NonNull |
|||
private List<SearchItemOrHeader> sectionAndSort(@NonNull final List<SearchItem> 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<SearchItem> 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<SearchItemOrHeader> 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); |
|||
} |
|||
} |
|||
} |
@ -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); |
|||
} |
|||
} |
@ -0,0 +1,209 @@ |
|||
package awais.instagrabber.adapters.viewholder; |
|||
|
|||
import android.content.Context; |
|||
import android.content.res.Resources; |
|||
import android.util.TypedValue; |
|||
import android.view.Menu; |
|||
import android.view.View; |
|||
|
|||
import androidx.annotation.ColorInt; |
|||
import androidx.annotation.NonNull; |
|||
import androidx.appcompat.view.ContextThemeWrapper; |
|||
import androidx.appcompat.widget.PopupMenu; |
|||
import androidx.core.content.ContextCompat; |
|||
import androidx.recyclerview.widget.RecyclerView; |
|||
|
|||
import awais.instagrabber.R; |
|||
import awais.instagrabber.adapters.CommentsAdapter.CommentCallback; |
|||
import awais.instagrabber.customviews.ProfilePicView; |
|||
import awais.instagrabber.databinding.ItemCommentBinding; |
|||
import awais.instagrabber.models.Comment; |
|||
import awais.instagrabber.repositories.responses.User; |
|||
import awais.instagrabber.utils.Utils; |
|||
|
|||
public final class CommentViewHolder extends RecyclerView.ViewHolder { |
|||
|
|||
private final ItemCommentBinding binding; |
|||
private final long currentUserId; |
|||
private final CommentCallback commentCallback; |
|||
@ColorInt |
|||
private int parentCommentHighlightColor; |
|||
private PopupMenu optionsPopup; |
|||
|
|||
public CommentViewHolder(@NonNull final ItemCommentBinding binding, |
|||
final long currentUserId, |
|||
final CommentCallback commentCallback) { |
|||
super(binding.getRoot()); |
|||
this.binding = binding; |
|||
this.currentUserId = currentUserId; |
|||
this.commentCallback = commentCallback; |
|||
final Context context = itemView.getContext(); |
|||
if (context == null) return; |
|||
final Resources.Theme theme = context.getTheme(); |
|||
if (theme == null) return; |
|||
final TypedValue typedValue = new TypedValue(); |
|||
final boolean resolved = theme.resolveAttribute(R.attr.parentCommentHighlightColor, typedValue, true); |
|||
if (resolved) { |
|||
parentCommentHighlightColor = typedValue.data; |
|||
} |
|||
} |
|||
|
|||
public void bind(final Comment comment, final boolean isReplyParent, final boolean isReply) { |
|||
if (comment == null) return; |
|||
itemView.setOnClickListener(v -> { |
|||
if (commentCallback != null) { |
|||
commentCallback.onClick(comment); |
|||
} |
|||
}); |
|||
if (isReplyParent && parentCommentHighlightColor != 0) { |
|||
itemView.setBackgroundColor(parentCommentHighlightColor); |
|||
} else { |
|||
itemView.setBackgroundColor(itemView.getResources().getColor(android.R.color.transparent)); |
|||
} |
|||
setupCommentText(comment, isReply); |
|||
binding.date.setText(comment.getDateTime()); |
|||
setLikes(comment, isReply); |
|||
setReplies(comment, isReply); |
|||
setUser(comment, isReply); |
|||
setupOptions(comment, isReply); |
|||
} |
|||
|
|||
private void setupCommentText(@NonNull final Comment comment, final boolean isReply) { |
|||
binding.comment.clearOnURLClickListeners(); |
|||
binding.comment.clearOnHashtagClickListeners(); |
|||
binding.comment.clearOnMentionClickListeners(); |
|||
binding.comment.clearOnEmailClickListeners(); |
|||
binding.comment.setText(comment.getText()); |
|||
binding.comment.setTextSize(TypedValue.COMPLEX_UNIT_SP, isReply ? 12 : 14); |
|||
binding.comment.addOnHashtagListener(autoLinkItem -> { |
|||
final String originalText = autoLinkItem.getOriginalText(); |
|||
if (commentCallback == null) return; |
|||
commentCallback.onHashtagClick(originalText); |
|||
}); |
|||
binding.comment.addOnMentionClickListener(autoLinkItem -> { |
|||
final String originalText = autoLinkItem.getOriginalText(); |
|||
if (commentCallback == null) return; |
|||
commentCallback.onMentionClick(originalText); |
|||
|
|||
}); |
|||
binding.comment.addOnEmailClickListener(autoLinkItem -> { |
|||
final String originalText = autoLinkItem.getOriginalText(); |
|||
if (commentCallback == null) return; |
|||
commentCallback.onEmailClick(originalText); |
|||
}); |
|||
binding.comment.addOnURLClickListener(autoLinkItem -> { |
|||
final String originalText = autoLinkItem.getOriginalText(); |
|||
if (commentCallback == null) return; |
|||
commentCallback.onURLClick(originalText); |
|||
}); |
|||
binding.comment.setOnLongClickListener(v -> { |
|||
Utils.copyText(itemView.getContext(), comment.getText()); |
|||
return true; |
|||
}); |
|||
binding.comment.setOnClickListener(v -> commentCallback.onClick(comment)); |
|||
} |
|||
|
|||
private void setUser(@NonNull final Comment comment, final boolean isReply) { |
|||
final User user = comment.getUser(); |
|||
if (user == null) return; |
|||
binding.username.setUsername(user.getUsername(), user.isVerified()); |
|||
binding.username.setTextAppearance(itemView.getContext(), isReply ? R.style.TextAppearance_MaterialComponents_Subtitle2 |
|||
: R.style.TextAppearance_MaterialComponents_Subtitle1); |
|||
binding.username.setOnClickListener(v -> { |
|||
if (commentCallback == null) return; |
|||
commentCallback.onMentionClick("@" + user.getUsername()); |
|||
}); |
|||
binding.profilePic.setImageURI(user.getProfilePicUrl()); |
|||
binding.profilePic.setSize(isReply ? ProfilePicView.Size.SMALLER : ProfilePicView.Size.SMALL); |
|||
binding.profilePic.setOnClickListener(v -> { |
|||
if (commentCallback == null) return; |
|||
commentCallback.onMentionClick("@" + user.getUsername()); |
|||
}); |
|||
} |
|||
|
|||
private void setLikes(@NonNull final Comment comment, final boolean isReply) { |
|||
// final String likesString = itemView.getResources().getQuantityString(R.plurals.likes_count, likes, likes); |
|||
binding.likes.setText(String.valueOf(comment.getLikes())); |
|||
binding.likes.setOnLongClickListener(v -> { |
|||
if (commentCallback == null) return false; |
|||
commentCallback.onViewLikes(comment); |
|||
return true; |
|||
}); |
|||
if (currentUserId == 0) { // not logged in |
|||
binding.likes.setOnClickListener(v -> { |
|||
if (commentCallback == null) return; |
|||
commentCallback.onViewLikes(comment); |
|||
}); |
|||
return; |
|||
} |
|||
final boolean liked = comment.getLiked(); |
|||
final int resId = liked ? R.drawable.ic_like : R.drawable.ic_not_liked; |
|||
binding.likes.setCompoundDrawablesRelativeWithSize(ContextCompat.getDrawable(itemView.getContext(), resId), null, null, null); |
|||
binding.likes.setOnClickListener(v -> { |
|||
if (commentCallback == null) return; |
|||
// toggle like |
|||
commentCallback.onLikeClick(comment, !liked, isReply); |
|||
}); |
|||
} |
|||
|
|||
private void setReplies(@NonNull final Comment comment, final boolean isReply) { |
|||
final int replies = comment.getReplyCount(); |
|||
binding.replies.setVisibility(View.VISIBLE); |
|||
final String text = isReply ? "" : String.valueOf(replies); |
|||
// final String string = itemView.getResources().getQuantityString(R.plurals.replies_count, replies, replies); |
|||
binding.replies.setText(text); |
|||
binding.replies.setOnClickListener(v -> { |
|||
if (commentCallback == null) return; |
|||
commentCallback.onRepliesClick(comment); |
|||
}); |
|||
} |
|||
|
|||
private void setupOptions(final Comment comment, final boolean isReply) { |
|||
binding.options.setOnClickListener(v -> { |
|||
if (optionsPopup == null) { |
|||
createOptionsPopupMenu(comment, isReply); |
|||
} |
|||
if (optionsPopup == null) return; |
|||
optionsPopup.show(); |
|||
}); |
|||
} |
|||
|
|||
private void createOptionsPopupMenu(final Comment comment, final boolean isReply) { |
|||
if (optionsPopup == null) { |
|||
final ContextThemeWrapper themeWrapper = new ContextThemeWrapper(itemView.getContext(), R.style.popupMenuStyle); |
|||
optionsPopup = new PopupMenu(themeWrapper, binding.options); |
|||
} else { |
|||
optionsPopup.getMenu().clear(); |
|||
} |
|||
optionsPopup.getMenuInflater().inflate(R.menu.comment_options_menu, optionsPopup.getMenu()); |
|||
final User user = comment.getUser(); |
|||
if (currentUserId == 0 || user == null || user.getPk() != currentUserId) { |
|||
final Menu menu = optionsPopup.getMenu(); |
|||
menu.removeItem(R.id.delete); |
|||
} |
|||
optionsPopup.setOnMenuItemClickListener(item -> { |
|||
if (commentCallback == null) return false; |
|||
int itemId = item.getItemId(); |
|||
if (itemId == R.id.translate) { |
|||
commentCallback.onTranslate(comment); |
|||
return true; |
|||
} |
|||
if (itemId == R.id.delete) { |
|||
commentCallback.onDelete(comment, isReply); |
|||
} |
|||
return true; |
|||
}); |
|||
} |
|||
|
|||
// private void setupReply(final Comment comment) { |
|||
// if (!isLoggedIn) { |
|||
// binding.reply.setVisibility(View.GONE); |
|||
// return; |
|||
// } |
|||
// binding.reply.setOnClickListener(v -> { |
|||
// if (commentCallback == null) return; |
|||
// // toggle like |
|||
// commentCallback.onReplyClick(comment); |
|||
// }); |
|||
// } |
|||
} |
@ -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); |
|||
} |
|||
} |
@ -1,95 +0,0 @@ |
|||
package awais.instagrabber.adapters.viewholder.comments; |
|||
|
|||
import android.view.View; |
|||
|
|||
import androidx.annotation.NonNull; |
|||
import androidx.recyclerview.widget.RecyclerView; |
|||
|
|||
import awais.instagrabber.R; |
|||
import awais.instagrabber.adapters.CommentsAdapter.CommentCallback; |
|||
import awais.instagrabber.databinding.ItemCommentSmallBinding; |
|||
import awais.instagrabber.models.CommentModel; |
|||
import awais.instagrabber.repositories.responses.User; |
|||
import awais.instagrabber.utils.Utils; |
|||
|
|||
public final class ChildCommentViewHolder extends RecyclerView.ViewHolder { |
|||
|
|||
private final ItemCommentSmallBinding binding; |
|||
|
|||
public ChildCommentViewHolder(@NonNull final ItemCommentSmallBinding binding) { |
|||
super(binding.getRoot()); |
|||
this.binding = binding; |
|||
} |
|||
|
|||
public void bind(final CommentModel comment, |
|||
final boolean selected, |
|||
final CommentCallback commentCallback) { |
|||
if (comment == null) return; |
|||
if (commentCallback != null) { |
|||
itemView.setOnClickListener(v -> commentCallback.onClick(comment)); |
|||
} |
|||
if (selected) { |
|||
itemView.setBackgroundColor(itemView.getResources().getColor(R.color.comment_selected)); |
|||
} else { |
|||
itemView.setBackgroundColor(itemView.getResources().getColor(android.R.color.transparent)); |
|||
} |
|||
setupCommentText(comment, commentCallback); |
|||
binding.tvDate.setText(comment.getDateTime()); |
|||
setLiked(comment.getLiked()); |
|||
setLikes((int) comment.getLikes()); |
|||
setUser(comment); |
|||
} |
|||
|
|||
private void setupCommentText(final CommentModel comment, final CommentCallback commentCallback) { |
|||
binding.tvComment.clearOnURLClickListeners(); |
|||
binding.tvComment.clearOnHashtagClickListeners(); |
|||
binding.tvComment.clearOnMentionClickListeners(); |
|||
binding.tvComment.clearOnEmailClickListeners(); |
|||
binding.tvComment.setText(comment.getText()); |
|||
binding.tvComment.addOnHashtagListener(autoLinkItem -> { |
|||
final String originalText = autoLinkItem.getOriginalText(); |
|||
if (commentCallback == null) return; |
|||
commentCallback.onHashtagClick(originalText); |
|||
}); |
|||
binding.tvComment.addOnMentionClickListener(autoLinkItem -> { |
|||
final String originalText = autoLinkItem.getOriginalText(); |
|||
if (commentCallback == null) return; |
|||
commentCallback.onMentionClick(originalText); |
|||
|
|||
}); |
|||
binding.tvComment.addOnEmailClickListener(autoLinkItem -> { |
|||
final String originalText = autoLinkItem.getOriginalText(); |
|||
if (commentCallback == null) return; |
|||
commentCallback.onEmailClick(originalText); |
|||
}); |
|||
binding.tvComment.addOnURLClickListener(autoLinkItem -> { |
|||
final String originalText = autoLinkItem.getOriginalText(); |
|||
if (commentCallback == null) return; |
|||
commentCallback.onURLClick(originalText); |
|||
}); |
|||
binding.tvComment.setOnLongClickListener(v -> { |
|||
Utils.copyText(itemView.getContext(), comment.getText()); |
|||
return true; |
|||
}); |
|||
binding.tvComment.setOnClickListener(v -> commentCallback.onClick(comment)); |
|||
} |
|||
|
|||
private void setUser(final CommentModel comment) { |
|||
final User profileModel = comment.getProfileModel(); |
|||
if (profileModel == null) return; |
|||
binding.tvUsername.setText(profileModel.getUsername()); |
|||
binding.ivProfilePic.setImageURI(profileModel.getProfilePicUrl()); |
|||
binding.isVerified.setVisibility(profileModel.isVerified() ? View.VISIBLE : View.GONE); |
|||
} |
|||
|
|||
private void setLikes(final int likes) { |
|||
final String likesString = itemView.getResources().getQuantityString(R.plurals.likes_count, likes, likes); |
|||
binding.tvLikes.setText(likesString); |
|||
} |
|||
|
|||
public final void setLiked(final boolean liked) { |
|||
if (liked) { |
|||
itemView.setBackgroundColor(itemView.getResources().getColor(R.color.comment_liked)); |
|||
} |
|||
} |
|||
} |
@ -1,95 +0,0 @@ |
|||
package awais.instagrabber.adapters.viewholder.comments; |
|||
|
|||
import android.view.View; |
|||
|
|||
import androidx.annotation.NonNull; |
|||
import androidx.recyclerview.widget.RecyclerView; |
|||
|
|||
import awais.instagrabber.R; |
|||
import awais.instagrabber.adapters.CommentsAdapter.CommentCallback; |
|||
import awais.instagrabber.databinding.ItemCommentBinding; |
|||
import awais.instagrabber.models.CommentModel; |
|||
import awais.instagrabber.repositories.responses.User; |
|||
import awais.instagrabber.utils.Utils; |
|||
|
|||
public final class ParentCommentViewHolder extends RecyclerView.ViewHolder { |
|||
|
|||
private final ItemCommentBinding binding; |
|||
|
|||
public ParentCommentViewHolder(@NonNull final ItemCommentBinding binding) { |
|||
super(binding.getRoot()); |
|||
this.binding = binding; |
|||
} |
|||
|
|||
public void bind(final CommentModel comment, |
|||
final boolean selected, |
|||
final CommentCallback commentCallback) { |
|||
if (comment == null) return; |
|||
if (commentCallback != null) { |
|||
itemView.setOnClickListener(v -> commentCallback.onClick(comment)); |
|||
} |
|||
if (selected) { |
|||
itemView.setBackgroundColor(itemView.getResources().getColor(R.color.comment_selected)); |
|||
} else { |
|||
itemView.setBackgroundColor(itemView.getResources().getColor(android.R.color.transparent)); |
|||
} |
|||
setupCommentText(comment, commentCallback); |
|||
binding.tvDate.setText(comment.getDateTime()); |
|||
setLiked(comment.getLiked()); |
|||
setLikes((int) comment.getLikes()); |
|||
setUser(comment); |
|||
} |
|||
|
|||
private void setupCommentText(final CommentModel comment, final CommentCallback commentCallback) { |
|||
binding.tvComment.clearOnURLClickListeners(); |
|||
binding.tvComment.clearOnHashtagClickListeners(); |
|||
binding.tvComment.clearOnMentionClickListeners(); |
|||
binding.tvComment.clearOnEmailClickListeners(); |
|||
binding.tvComment.setText(comment.getText()); |
|||
binding.tvComment.addOnHashtagListener(autoLinkItem -> { |
|||
final String originalText = autoLinkItem.getOriginalText(); |
|||
if (commentCallback == null) return; |
|||
commentCallback.onHashtagClick(originalText); |
|||
}); |
|||
binding.tvComment.addOnMentionClickListener(autoLinkItem -> { |
|||
final String originalText = autoLinkItem.getOriginalText(); |
|||
if (commentCallback == null) return; |
|||
commentCallback.onMentionClick(originalText); |
|||
|
|||
}); |
|||
binding.tvComment.addOnEmailClickListener(autoLinkItem -> { |
|||
final String originalText = autoLinkItem.getOriginalText(); |
|||
if (commentCallback == null) return; |
|||
commentCallback.onEmailClick(originalText); |
|||
}); |
|||
binding.tvComment.addOnURLClickListener(autoLinkItem -> { |
|||
final String originalText = autoLinkItem.getOriginalText(); |
|||
if (commentCallback == null) return; |
|||
commentCallback.onURLClick(originalText); |
|||
}); |
|||
binding.tvComment.setOnLongClickListener(v -> { |
|||
Utils.copyText(itemView.getContext(), comment.getText()); |
|||
return true; |
|||
}); |
|||
binding.tvComment.setOnClickListener(v -> commentCallback.onClick(comment)); |
|||
} |
|||
|
|||
private void setUser(final CommentModel comment) { |
|||
final User profileModel = comment.getProfileModel(); |
|||
if (profileModel == null) return; |
|||
binding.tvUsername.setText(profileModel.getUsername()); |
|||
binding.ivProfilePic.setImageURI(profileModel.getProfilePicUrl()); |
|||
binding.isVerified.setVisibility(profileModel.isVerified() ? View.VISIBLE : View.GONE); |
|||
} |
|||
|
|||
private void setLikes(final int likes) { |
|||
final String likesString = itemView.getResources().getQuantityString(R.plurals.likes_count, likes, likes); |
|||
binding.tvLikes.setText(likesString); |
|||
} |
|||
|
|||
public final void setLiked(final boolean liked) { |
|||
if (liked) { |
|||
itemView.setBackgroundColor(itemView.getResources().getColor(R.color.comment_liked)); |
|||
} |
|||
} |
|||
} |
@ -1,268 +0,0 @@ |
|||
package awais.instagrabber.asyncs; |
|||
|
|||
import android.os.AsyncTask; |
|||
import android.util.Log; |
|||
import android.util.Pair; |
|||
|
|||
import androidx.annotation.NonNull; |
|||
|
|||
import org.json.JSONArray; |
|||
import org.json.JSONObject; |
|||
|
|||
import java.net.HttpURLConnection; |
|||
import java.net.URL; |
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
|
|||
import awais.instagrabber.BuildConfig; |
|||
import awais.instagrabber.interfaces.FetchListener; |
|||
import awais.instagrabber.models.CommentModel; |
|||
import awais.instagrabber.repositories.responses.FriendshipStatus; |
|||
import awais.instagrabber.repositories.responses.User; |
|||
import awais.instagrabber.utils.Constants; |
|||
import awais.instagrabber.utils.NetworkUtils; |
|||
import awais.instagrabber.utils.TextUtils; |
|||
//import awaisomereport.LogCollector; |
|||
|
|||
//import static awais.instagrabber.utils.Utils.logCollector; |
|||
|
|||
public final class CommentsFetcher extends AsyncTask<Void, Void, List<CommentModel>> { |
|||
private static final String TAG = "CommentsFetcher"; |
|||
|
|||
private final String shortCode, endCursor; |
|||
private final FetchListener<List<CommentModel>> fetchListener; |
|||
|
|||
public CommentsFetcher(final String shortCode, final String endCursor, final FetchListener<List<CommentModel>> fetchListener) { |
|||
this.shortCode = shortCode; |
|||
this.endCursor = endCursor; |
|||
this.fetchListener = fetchListener; |
|||
} |
|||
|
|||
@NonNull |
|||
@Override |
|||
protected List<CommentModel> doInBackground(final Void... voids) { |
|||
/* |
|||
"https://www.instagram.com/graphql/query/?query_hash=97b41c52301f77ce508f55e66d17620e&variables=" + "{\"shortcode\":\"" + shortcode + "\",\"first\":50,\"after\":\"" + endCursor + "\"}"; |
|||
|
|||
97b41c52301f77ce508f55e66d17620e -> for comments |
|||
51fdd02b67508306ad4484ff574a0b62 -> for child comments |
|||
|
|||
https://www.instagram.com/graphql/query/?query_hash=51fdd02b67508306ad4484ff574a0b62&variables={"comment_id":"18100041898085322","first":50,"after":""} |
|||
*/ |
|||
final List<CommentModel> commentModels = getParentComments(); |
|||
if (commentModels != null) { |
|||
for (final CommentModel commentModel : commentModels) { |
|||
final List<CommentModel> childCommentModels = commentModel.getChildCommentModels(); |
|||
if (childCommentModels != null) { |
|||
final int childCommentsLen = childCommentModels.size(); |
|||
final CommentModel lastChild = childCommentModels.get(childCommentsLen - 1); |
|||
if (lastChild != null && lastChild.hasNextPage() && !TextUtils.isEmpty(lastChild.getEndCursor())) { |
|||
final List<CommentModel> remoteChildComments = getChildComments(commentModel.getId()); |
|||
commentModel.setChildCommentModels(remoteChildComments); |
|||
lastChild.setPageCursor(false, null); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
return commentModels; |
|||
} |
|||
|
|||
@Override |
|||
protected void onPreExecute() { |
|||
if (fetchListener != null) fetchListener.doBefore(); |
|||
} |
|||
|
|||
@Override |
|||
protected void onPostExecute(final List<CommentModel> result) { |
|||
if (fetchListener != null) fetchListener.onResult(result); |
|||
} |
|||
|
|||
@NonNull |
|||
private synchronized List<CommentModel> getChildComments(final String commentId) { |
|||
final List<CommentModel> commentModels = new ArrayList<>(); |
|||
String childEndCursor = ""; |
|||
while (childEndCursor != null) { |
|||
final String url = "https://www.instagram.com/graphql/query/?query_hash=51fdd02b67508306ad4484ff574a0b62&variables=" + |
|||
"{\"comment_id\":\"" + commentId + "\",\"first\":50,\"after\":\"" + childEndCursor + "\"}"; |
|||
try { |
|||
final HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); |
|||
conn.setUseCaches(false); |
|||
conn.connect(); |
|||
|
|||
if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) break; |
|||
else { |
|||
final JSONObject data = new JSONObject(NetworkUtils.readFromConnection(conn)).getJSONObject("data") |
|||
.getJSONObject("comment") |
|||
.getJSONObject("edge_threaded_comments"); |
|||
|
|||
final JSONObject pageInfo = data.getJSONObject("page_info"); |
|||
childEndCursor = pageInfo.getString("end_cursor"); |
|||
if (TextUtils.isEmpty(childEndCursor)) childEndCursor = null; |
|||
|
|||
final JSONArray childComments = data.optJSONArray("edges"); |
|||
if (childComments != null) { |
|||
final int length = childComments.length(); |
|||
for (int i = 0; i < length; ++i) { |
|||
final JSONObject childComment = childComments.getJSONObject(i).optJSONObject("node"); |
|||
|
|||
if (childComment != null) { |
|||
final JSONObject owner = childComment.getJSONObject("owner"); |
|||
final User user = new User( |
|||
owner.optLong(Constants.EXTRAS_ID, 0), |
|||
owner.getString(Constants.EXTRAS_USERNAME), |
|||
null, |
|||
false, |
|||
owner.getString("profile_pic_url"), |
|||
null, |
|||
new FriendshipStatus(false, false, false, false, false, false, false, false, false, false), |
|||
false, false, false, false, false, null, null, 0, 0, 0, 0, null, null, 0, null, null, null, |
|||
null, null, null); |
|||
final JSONObject likedBy = childComment.optJSONObject("edge_liked_by"); |
|||
commentModels.add(new CommentModel(childComment.getString(Constants.EXTRAS_ID), |
|||
childComment.getString("text"), |
|||
childComment.getLong("created_at"), |
|||
likedBy != null ? likedBy.optLong("count", 0) : 0, |
|||
childComment.getBoolean("viewer_has_liked"), |
|||
user)); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
conn.disconnect(); |
|||
} catch (final Exception e) { |
|||
// if (logCollector != null) |
|||
// logCollector.appendException(e, |
|||
// LogCollector.LogFile.ASYNC_COMMENTS_FETCHER, |
|||
// "getChildComments", |
|||
// new Pair<>("commentModels.size", commentModels.size())); |
|||
if (BuildConfig.DEBUG) Log.e(TAG, "", e); |
|||
if (fetchListener != null) fetchListener.onFailure(e); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
return commentModels; |
|||
} |
|||
|
|||
@NonNull |
|||
private synchronized List<CommentModel> getParentComments() { |
|||
final List<CommentModel> commentModels = new ArrayList<>(); |
|||
final String url = "https://www.instagram.com/graphql/query/?query_hash=bc3296d1ce80a24b1b6e40b1e72903f5&variables=" + |
|||
"{\"shortcode\":\"" + shortCode + "\",\"first\":50,\"after\":\"" + endCursor.replace("\"", "\\\"") + "\"}"; |
|||
try { |
|||
final HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); |
|||
conn.setUseCaches(false); |
|||
conn.connect(); |
|||
|
|||
if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) return null; |
|||
else { |
|||
final JSONObject parentComments = new JSONObject(NetworkUtils.readFromConnection(conn)).getJSONObject("data") |
|||
.getJSONObject("shortcode_media") |
|||
.getJSONObject( |
|||
"edge_media_to_parent_comment"); |
|||
|
|||
final JSONObject pageInfo = parentComments.getJSONObject("page_info"); |
|||
final String foundEndCursor = pageInfo.optString("end_cursor"); |
|||
final boolean hasNextPage = pageInfo.optBoolean("has_next_page", !TextUtils.isEmpty(foundEndCursor)); |
|||
|
|||
// final boolean containsToken = endCursor.contains("bifilter_token"); |
|||
// if (!Utils.isEmpty(endCursor) && (containsToken || endCursor.contains("cached_comments_cursor"))) { |
|||
// final JSONObject endCursorObject = new JSONObject(endCursor); |
|||
// endCursor = endCursorObject.optString("cached_comments_cursor"); |
|||
// |
|||
// if (!Utils.isEmpty(endCursor)) |
|||
// endCursor = "{\\\"cached_comments_cursor\\\": \\\"" + endCursor + "\\\", "; |
|||
// else |
|||
// endCursor = "{"; |
|||
// |
|||
// endCursor = endCursor + "\\\"bifilter_token\\\": \\\"" + endCursorObject.getString("bifilter_token") + "\\\"}"; |
|||
// } |
|||
// else if (containsToken) endCursor = null; |
|||
|
|||
final JSONArray comments = parentComments.getJSONArray("edges"); |
|||
final int commentsLen = comments.length(); |
|||
for (int i = 0; i < commentsLen; ++i) { |
|||
final JSONObject comment = comments.getJSONObject(i).getJSONObject("node"); |
|||
|
|||
final JSONObject owner = comment.getJSONObject("owner"); |
|||
final User user = new User( |
|||
owner.optLong(Constants.EXTRAS_ID, 0), |
|||
owner.getString(Constants.EXTRAS_USERNAME), |
|||
null, |
|||
false, |
|||
owner.getString("profile_pic_url"), |
|||
null, |
|||
new FriendshipStatus(false, false, false, false, false, false, false, false, false, false), |
|||
owner.optBoolean("is_verified"), |
|||
false, false, false, false, null, null, 0, 0, 0, 0, null, null, 0, null, null, null, null, |
|||
null, null); |
|||
final JSONObject likedBy = comment.optJSONObject("edge_liked_by"); |
|||
final String commentId = comment.getString(Constants.EXTRAS_ID); |
|||
final CommentModel commentModel = new CommentModel(commentId, |
|||
comment.getString("text"), |
|||
comment.getLong("created_at"), |
|||
likedBy != null ? likedBy.optLong("count", 0) : 0, |
|||
comment.getBoolean("viewer_has_liked"), |
|||
user); |
|||
if (i == 0 && !foundEndCursor.contains("tao_cursor")) |
|||
commentModel.setPageCursor(hasNextPage, TextUtils.isEmpty(foundEndCursor) ? null : foundEndCursor); |
|||
JSONObject tempJsonObject; |
|||
final JSONArray childCommentsArray; |
|||
final int childCommentsLen; |
|||
if ((tempJsonObject = comment.optJSONObject("edge_threaded_comments")) != null && |
|||
(childCommentsArray = tempJsonObject.optJSONArray("edges")) != null |
|||
&& (childCommentsLen = childCommentsArray.length()) > 0) { |
|||
|
|||
final String childEndCursor; |
|||
final boolean childHasNextPage; |
|||
if ((tempJsonObject = tempJsonObject.optJSONObject("page_info")) != null) { |
|||
childEndCursor = tempJsonObject.optString("end_cursor"); |
|||
childHasNextPage = tempJsonObject.optBoolean("has_next_page", !TextUtils.isEmpty(childEndCursor)); |
|||
} else { |
|||
childEndCursor = null; |
|||
childHasNextPage = false; |
|||
} |
|||
|
|||
final List<CommentModel> childCommentModels = new ArrayList<>(); |
|||
for (int j = 0; j < childCommentsLen; ++j) { |
|||
final JSONObject childComment = childCommentsArray.getJSONObject(j).getJSONObject("node"); |
|||
|
|||
tempJsonObject = childComment.getJSONObject("owner"); |
|||
final User childUser = new User( |
|||
tempJsonObject.optLong(Constants.EXTRAS_ID, 0), |
|||
tempJsonObject.getString(Constants.EXTRAS_USERNAME), |
|||
null, |
|||
false, |
|||
tempJsonObject.getString("profile_pic_url"), |
|||
null, |
|||
new FriendshipStatus(false, false, false, false, false, false, false, false, false, false), |
|||
tempJsonObject.optBoolean("is_verified"), false, false, false, false, null, null, 0, 0, 0, 0, null, null, 0, |
|||
null, null, null, null, null, null); |
|||
|
|||
tempJsonObject = childComment.optJSONObject("edge_liked_by"); |
|||
childCommentModels.add(new CommentModel(childComment.getString(Constants.EXTRAS_ID), |
|||
childComment.getString("text"), |
|||
childComment.getLong("created_at"), |
|||
tempJsonObject != null ? tempJsonObject.optLong("count", 0) : 0, |
|||
childComment.getBoolean("viewer_has_liked"), |
|||
childUser)); |
|||
} |
|||
childCommentModels.get(childCommentsLen - 1).setPageCursor(childHasNextPage, childEndCursor); |
|||
commentModel.setChildCommentModels(childCommentModels); |
|||
} |
|||
commentModels.add(commentModel); |
|||
} |
|||
} |
|||
|
|||
conn.disconnect(); |
|||
} catch (final Exception e) { |
|||
// if (logCollector != null) |
|||
// logCollector.appendException(e, LogCollector.LogFile.ASYNC_COMMENTS_FETCHER, "getParentComments", |
|||
// new Pair<>("commentModelsList.size", commentModels.size())); |
|||
if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e); |
|||
if (fetchListener != null) fetchListener.onFailure(e); |
|||
return null; |
|||
} |
|||
return commentModels; |
|||
} |
|||
} |
@ -0,0 +1,165 @@ |
|||
package awais.instagrabber.customviews; |
|||
|
|||
import android.content.Context; |
|||
import android.util.AttributeSet; |
|||
import android.util.Log; |
|||
import android.view.ViewGroup; |
|||
|
|||
import androidx.annotation.NonNull; |
|||
import androidx.annotation.Nullable; |
|||
import androidx.appcompat.widget.AppCompatTextView; |
|||
import androidx.transition.ChangeBounds; |
|||
import androidx.transition.Transition; |
|||
import androidx.transition.TransitionManager; |
|||
import androidx.transition.TransitionSet; |
|||
|
|||
import java.time.Duration; |
|||
|
|||
import awais.instagrabber.customviews.helpers.ChangeText; |
|||
import awais.instagrabber.utils.NumberUtils; |
|||
|
|||
public class FormattedNumberTextView extends AppCompatTextView { |
|||
private static final String TAG = FormattedNumberTextView.class.getSimpleName(); |
|||
private static final Transition TRANSITION; |
|||
|
|||
private long number = Long.MIN_VALUE; |
|||
private boolean showAbbreviation = true; |
|||
private boolean animateChanges = false; |
|||
private boolean toggleOnClick = true; |
|||
private boolean autoToggleToAbbreviation = true; |
|||
private long autoToggleTimeoutMs = Duration.ofSeconds(2).toMillis(); |
|||
private boolean initDone = false; |
|||
|
|||
static { |
|||
final TransitionSet transitionSet = new TransitionSet(); |
|||
final ChangeText changeText = new ChangeText().setChangeBehavior(ChangeText.CHANGE_BEHAVIOR_OUT_IN); |
|||
transitionSet.addTransition(changeText).addTransition(new ChangeBounds()); |
|||
TRANSITION = transitionSet; |
|||
} |
|||
|
|||
|
|||
public FormattedNumberTextView(@NonNull final Context context) { |
|||
super(context); |
|||
init(); |
|||
} |
|||
|
|||
public FormattedNumberTextView(@NonNull final Context context, @Nullable final AttributeSet attrs) { |
|||
super(context, attrs); |
|||
init(); |
|||
} |
|||
|
|||
public FormattedNumberTextView(@NonNull final Context context, @Nullable final AttributeSet attrs, final int defStyleAttr) { |
|||
super(context, attrs, defStyleAttr); |
|||
init(); |
|||
} |
|||
|
|||
private void init() { |
|||
if (initDone) return; |
|||
setupClickToggle(); |
|||
initDone = true; |
|||
} |
|||
|
|||
private void setupClickToggle() { |
|||
setOnClickListener(null); |
|||
} |
|||
|
|||
private OnClickListener getWrappedClickListener(@Nullable final OnClickListener l) { |
|||
if (!toggleOnClick) { |
|||
return l; |
|||
} |
|||
return v -> { |
|||
toggleAbbreviation(); |
|||
if (l != null) { |
|||
l.onClick(this); |
|||
} |
|||
}; |
|||
} |
|||
|
|||
public void setNumber(final long number) { |
|||
if (this.number == number) return; |
|||
this.number = number; |
|||
format(); |
|||
} |
|||
|
|||
public void clearNumber() { |
|||
if (number == Long.MIN_VALUE) return; |
|||
number = Long.MIN_VALUE; |
|||
format(); |
|||
} |
|||
|
|||
public void setShowAbbreviation(final boolean showAbbreviation) { |
|||
if (this.showAbbreviation && showAbbreviation) return; |
|||
this.showAbbreviation = showAbbreviation; |
|||
format(); |
|||
} |
|||
|
|||
public boolean isShowAbbreviation() { |
|||
return showAbbreviation; |
|||
} |
|||
|
|||
private void toggleAbbreviation() { |
|||
if (number == Long.MIN_VALUE) return; |
|||
setShowAbbreviation(!showAbbreviation); |
|||
} |
|||
|
|||
public void setToggleOnClick(final boolean toggleOnClick) { |
|||
this.toggleOnClick = toggleOnClick; |
|||
} |
|||
|
|||
public boolean isToggleOnClick() { |
|||
return toggleOnClick; |
|||
} |
|||
|
|||
public void setAutoToggleToAbbreviation(final boolean autoToggleToAbbreviation) { |
|||
this.autoToggleToAbbreviation = autoToggleToAbbreviation; |
|||
} |
|||
|
|||
public boolean isAutoToggleToAbbreviation() { |
|||
return autoToggleToAbbreviation; |
|||
} |
|||
|
|||
public void setAutoToggleTimeoutMs(final long autoToggleTimeoutMs) { |
|||
this.autoToggleTimeoutMs = autoToggleTimeoutMs; |
|||
} |
|||
|
|||
public long getAutoToggleTimeoutMs() { |
|||
return autoToggleTimeoutMs; |
|||
} |
|||
|
|||
public void setAnimateChanges(final boolean animateChanges) { |
|||
this.animateChanges = animateChanges; |
|||
} |
|||
|
|||
public boolean isAnimateChanges() { |
|||
return animateChanges; |
|||
} |
|||
|
|||
@Override |
|||
public void setOnClickListener(@Nullable final OnClickListener l) { |
|||
super.setOnClickListener(getWrappedClickListener(l)); |
|||
} |
|||
|
|||
private void format() { |
|||
post(() -> { |
|||
if (animateChanges) { |
|||
try { |
|||
TransitionManager.beginDelayedTransition((ViewGroup) getParent(), TRANSITION); |
|||
} catch (Exception e) { |
|||
Log.e(TAG, "format: ", e); |
|||
} |
|||
} |
|||
if (number == Long.MIN_VALUE) { |
|||
setText(null); |
|||
return; |
|||
} |
|||
if (showAbbreviation) { |
|||
setText(NumberUtils.abbreviate(number)); |
|||
return; |
|||
} |
|||
setText(String.valueOf(number)); |
|||
if (autoToggleToAbbreviation) { |
|||
getHandler().postDelayed(() -> setShowAbbreviation(true), autoToggleTimeoutMs); |
|||
} |
|||
}); |
|||
} |
|||
} |
@ -0,0 +1,75 @@ |
|||
package awais.instagrabber.customviews; |
|||
|
|||
import android.content.Context; |
|||
import android.os.Bundle; |
|||
|
|||
import androidx.annotation.NonNull; |
|||
import androidx.annotation.Nullable; |
|||
import androidx.fragment.app.FragmentManager; |
|||
import androidx.navigation.NavDestination; |
|||
import androidx.navigation.NavOptions; |
|||
import androidx.navigation.Navigator; |
|||
import androidx.navigation.fragment.FragmentNavigator; |
|||
|
|||
import awais.instagrabber.R; |
|||
|
|||
@Navigator.Name("fragment") |
|||
public class FragmentNavigatorWithDefaultAnimations extends FragmentNavigator { |
|||
|
|||
private final NavOptions emptyNavOptions = new NavOptions.Builder().build(); |
|||
// private final NavOptions defaultNavOptions = new NavOptions.Builder() |
|||
// .setEnterAnim(R.animator.nav_default_enter_anim) |
|||
// .setExitAnim(R.animator.nav_default_exit_anim) |
|||
// .setPopEnterAnim(R.animator.nav_default_pop_enter_anim) |
|||
// .setPopExitAnim(R.animator.nav_default_pop_exit_anim) |
|||
// .build(); |
|||
|
|||
private final NavOptions defaultNavOptions = new NavOptions.Builder() |
|||
.setEnterAnim(R.anim.slide_in_right) |
|||
.setExitAnim(R.anim.slide_out_left) |
|||
.setPopEnterAnim(android.R.anim.slide_in_left) |
|||
.setPopExitAnim(android.R.anim.slide_out_right) |
|||
.build(); |
|||
|
|||
public FragmentNavigatorWithDefaultAnimations(@NonNull final Context context, |
|||
@NonNull final FragmentManager manager, |
|||
final int containerId) { |
|||
super(context, manager, containerId); |
|||
} |
|||
|
|||
@Nullable |
|||
@Override |
|||
public NavDestination navigate(@NonNull final Destination destination, |
|||
@Nullable final Bundle args, |
|||
@Nullable final NavOptions navOptions, |
|||
@Nullable final Navigator.Extras navigatorExtras) { |
|||
// this will try to fill in empty animations with defaults when no shared element transitions are set |
|||
// https://developer.android.com/guide/navigation/navigation-animate-transitions#shared-element |
|||
final boolean shouldUseTransitionsInstead = navigatorExtras != null; |
|||
final NavOptions navOptions1 = shouldUseTransitionsInstead ? navOptions : fillEmptyAnimationsWithDefaults(navOptions); |
|||
return super.navigate(destination, args, navOptions1, navigatorExtras); |
|||
} |
|||
|
|||
private NavOptions fillEmptyAnimationsWithDefaults(@Nullable final NavOptions navOptions) { |
|||
if (navOptions == null) { |
|||
return defaultNavOptions; |
|||
} |
|||
return copyNavOptionsWithDefaultAnimations(navOptions); |
|||
} |
|||
|
|||
@NonNull |
|||
private NavOptions copyNavOptionsWithDefaultAnimations(@NonNull final NavOptions navOptions) { |
|||
return new NavOptions.Builder() |
|||
.setLaunchSingleTop(navOptions.shouldLaunchSingleTop()) |
|||
.setPopUpTo(navOptions.getPopUpTo(), navOptions.isPopUpToInclusive()) |
|||
.setEnterAnim(navOptions.getEnterAnim() == emptyNavOptions.getEnterAnim() |
|||
? defaultNavOptions.getEnterAnim() : navOptions.getEnterAnim()) |
|||
.setExitAnim(navOptions.getExitAnim() == emptyNavOptions.getExitAnim() |
|||
? defaultNavOptions.getExitAnim() : navOptions.getExitAnim()) |
|||
.setPopEnterAnim(navOptions.getPopEnterAnim() == emptyNavOptions.getPopEnterAnim() |
|||
? defaultNavOptions.getPopEnterAnim() : navOptions.getPopEnterAnim()) |
|||
.setPopExitAnim(navOptions.getPopExitAnim() == emptyNavOptions.getPopExitAnim() |
|||
? defaultNavOptions.getPopExitAnim() : navOptions.getPopExitAnim()) |
|||
.build(); |
|||
} |
|||
} |
@ -0,0 +1,60 @@ |
|||
package awais.instagrabber.customviews; |
|||
|
|||
import android.os.Bundle; |
|||
|
|||
import androidx.annotation.NavigationRes; |
|||
import androidx.annotation.NonNull; |
|||
import androidx.annotation.Nullable; |
|||
import androidx.navigation.NavController; |
|||
import androidx.navigation.Navigator; |
|||
import androidx.navigation.fragment.FragmentNavigator; |
|||
import androidx.navigation.fragment.NavHostFragment; |
|||
|
|||
public class NavHostFragmentWithDefaultAnimations extends NavHostFragment { |
|||
private static final String KEY_GRAPH_ID = "android-support-nav:fragment:graphId"; |
|||
private static final String KEY_START_DESTINATION_ARGS = |
|||
"android-support-nav:fragment:startDestinationArgs"; |
|||
private static final String KEY_NAV_CONTROLLER_STATE = |
|||
"android-support-nav:fragment:navControllerState"; |
|||
private static final String KEY_DEFAULT_NAV_HOST = "android-support-nav:fragment:defaultHost"; |
|||
|
|||
@NonNull |
|||
public static NavHostFragment create(@NavigationRes int graphResId) { |
|||
return create(graphResId, null); |
|||
} |
|||
|
|||
@NonNull |
|||
public static NavHostFragment create(@NavigationRes int graphResId, |
|||
@Nullable Bundle startDestinationArgs) { |
|||
Bundle b = null; |
|||
if (graphResId != 0) { |
|||
b = new Bundle(); |
|||
b.putInt(KEY_GRAPH_ID, graphResId); |
|||
} |
|||
if (startDestinationArgs != null) { |
|||
if (b == null) { |
|||
b = new Bundle(); |
|||
} |
|||
b.putBundle(KEY_START_DESTINATION_ARGS, startDestinationArgs); |
|||
} |
|||
|
|||
final NavHostFragmentWithDefaultAnimations result = new NavHostFragmentWithDefaultAnimations(); |
|||
if (b != null) { |
|||
result.setArguments(b); |
|||
} |
|||
return result; |
|||
} |
|||
|
|||
@NonNull |
|||
@Override |
|||
protected Navigator<? extends FragmentNavigator.Destination> createFragmentNavigator() { |
|||
return new FragmentNavigatorWithDefaultAnimations(requireContext(), getChildFragmentManager(), getId()); |
|||
} |
|||
|
|||
@Override |
|||
protected void onCreateNavController(@NonNull final NavController navController) { |
|||
super.onCreateNavController(navController); |
|||
navController.getNavigatorProvider() |
|||
.addNavigator(new FragmentNavigatorWithDefaultAnimations(requireContext(), getChildFragmentManager(), getId())); |
|||
} |
|||
} |
@ -0,0 +1,95 @@ |
|||
package awais.instagrabber.customviews; |
|||
|
|||
import android.content.Context; |
|||
import android.content.res.TypedArray; |
|||
import android.graphics.Rect; |
|||
import android.graphics.drawable.Drawable; |
|||
import android.util.AttributeSet; |
|||
|
|||
import androidx.annotation.NonNull; |
|||
import androidx.annotation.Nullable; |
|||
import androidx.emoji.widget.EmojiAppCompatTextView; |
|||
|
|||
import awais.instagrabber.R; |
|||
|
|||
/** |
|||
* https://stackoverflow.com/a/31916731 |
|||
*/ |
|||
public class TextViewDrawableSize extends EmojiAppCompatTextView { |
|||
|
|||
private int mDrawableWidth; |
|||
private int mDrawableHeight; |
|||
private boolean calledFromInit = false; |
|||
|
|||
public TextViewDrawableSize(final Context context) { |
|||
this(context, null); |
|||
} |
|||
|
|||
public TextViewDrawableSize(final Context context, final AttributeSet attrs) { |
|||
this(context, attrs, 0); |
|||
} |
|||
|
|||
public TextViewDrawableSize(final Context context, final AttributeSet attrs, final int defStyleAttr) { |
|||
super(context, attrs, defStyleAttr); |
|||
init(context, attrs, defStyleAttr); |
|||
} |
|||
|
|||
private void init(@NonNull final Context context, final AttributeSet attrs, final int defStyleAttr) { |
|||
final TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.TextViewDrawableSize, defStyleAttr, 0); |
|||
|
|||
try { |
|||
mDrawableWidth = array.getDimensionPixelSize(R.styleable.TextViewDrawableSize_compoundDrawableWidth, -1); |
|||
mDrawableHeight = array.getDimensionPixelSize(R.styleable.TextViewDrawableSize_compoundDrawableHeight, -1); |
|||
} finally { |
|||
array.recycle(); |
|||
} |
|||
|
|||
if (mDrawableWidth > 0 || mDrawableHeight > 0) { |
|||
initCompoundDrawableSize(); |
|||
} |
|||
} |
|||
|
|||
private void initCompoundDrawableSize() { |
|||
final Drawable[] drawables = getCompoundDrawablesRelative(); |
|||
for (Drawable drawable : drawables) { |
|||
if (drawable == null) { |
|||
continue; |
|||
} |
|||
|
|||
final Rect realBounds = drawable.getBounds(); |
|||
float scaleFactor = drawable.getIntrinsicHeight() / (float) drawable.getIntrinsicWidth(); |
|||
|
|||
float drawableWidth = drawable.getIntrinsicWidth(); |
|||
float drawableHeight = drawable.getIntrinsicHeight(); |
|||
|
|||
if (mDrawableWidth > 0) { |
|||
// save scale factor of image |
|||
if (drawableWidth > mDrawableWidth) { |
|||
drawableWidth = mDrawableWidth; |
|||
drawableHeight = drawableWidth * scaleFactor; |
|||
} |
|||
} |
|||
if (mDrawableHeight > 0) { |
|||
// save scale factor of image |
|||
if (drawableHeight > mDrawableHeight) { |
|||
drawableHeight = mDrawableHeight; |
|||
drawableWidth = drawableHeight / scaleFactor; |
|||
} |
|||
} |
|||
|
|||
realBounds.right = realBounds.left + Math.round(drawableWidth); |
|||
realBounds.bottom = realBounds.top + Math.round(drawableHeight); |
|||
|
|||
drawable.setBounds(realBounds); |
|||
} |
|||
setCompoundDrawablesRelative(drawables[0], drawables[1], drawables[2], drawables[3]); |
|||
} |
|||
|
|||
public void setCompoundDrawablesRelativeWithSize(@Nullable final Drawable start, |
|||
@Nullable final Drawable top, |
|||
@Nullable final Drawable end, |
|||
@Nullable final Drawable bottom) { |
|||
setCompoundDrawablesRelative(start, top, end, bottom); |
|||
initCompoundDrawableSize(); |
|||
} |
|||
} |
@ -0,0 +1,77 @@ |
|||
package awais.instagrabber.customviews; |
|||
|
|||
import android.content.Context; |
|||
import android.graphics.drawable.Drawable; |
|||
import android.text.SpannableStringBuilder; |
|||
import android.text.Spanned; |
|||
import android.util.AttributeSet; |
|||
import android.util.Log; |
|||
|
|||
import androidx.annotation.NonNull; |
|||
import androidx.annotation.Nullable; |
|||
import androidx.appcompat.content.res.AppCompatResources; |
|||
import androidx.appcompat.widget.AppCompatTextView; |
|||
|
|||
import awais.instagrabber.R; |
|||
import awais.instagrabber.utils.Utils; |
|||
|
|||
public class UsernameTextView extends AppCompatTextView { |
|||
private static final String TAG = UsernameTextView.class.getSimpleName(); |
|||
|
|||
private final int drawableSize = Utils.convertDpToPx(24); |
|||
|
|||
private boolean verified; |
|||
private VerticalImageSpan verifiedSpan; |
|||
|
|||
public UsernameTextView(@NonNull final Context context) { |
|||
this(context, null); |
|||
} |
|||
|
|||
public UsernameTextView(@NonNull final Context context, @Nullable final AttributeSet attrs) { |
|||
this(context, attrs, 0); |
|||
} |
|||
|
|||
public UsernameTextView(@NonNull final Context context, @Nullable final AttributeSet attrs, final int defStyleAttr) { |
|||
super(context, attrs, defStyleAttr); |
|||
init(); |
|||
} |
|||
|
|||
private void init() { |
|||
try { |
|||
final Drawable verifiedDrawable = AppCompatResources.getDrawable(getContext(), R.drawable.verified); |
|||
final Drawable drawable = verifiedDrawable.mutate(); |
|||
drawable.setBounds(0, 0, drawableSize, drawableSize); |
|||
verifiedSpan = new VerticalImageSpan(drawable); |
|||
} catch (Exception e) { |
|||
Log.e(TAG, "init: ", e); |
|||
} |
|||
} |
|||
|
|||
public void setUsername(final CharSequence username) { |
|||
setUsername(username, false); |
|||
} |
|||
|
|||
public void setUsername(final CharSequence username, final boolean verified) { |
|||
this.verified = verified; |
|||
final SpannableStringBuilder sb = new SpannableStringBuilder(username); |
|||
if (verified) { |
|||
try { |
|||
if (verifiedSpan != null) { |
|||
sb.append(" "); |
|||
sb.setSpan(verifiedSpan, sb.length() - 1, sb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); |
|||
} |
|||
} catch (Exception e) { |
|||
Log.e(TAG, "bind: ", e); |
|||
} |
|||
} |
|||
super.setText(sb); |
|||
} |
|||
|
|||
public boolean isVerified() { |
|||
return verified; |
|||
} |
|||
|
|||
public void setVerified(final boolean verified) { |
|||
setUsername(getText(), verified); |
|||
} |
|||
} |
@ -0,0 +1,100 @@ |
|||
package awais.instagrabber.customviews.emoji; |
|||
|
|||
import android.app.Dialog; |
|||
import android.content.Context; |
|||
import android.os.Bundle; |
|||
import android.view.LayoutInflater; |
|||
import android.view.View; |
|||
import android.view.ViewGroup; |
|||
|
|||
import androidx.annotation.NonNull; |
|||
import androidx.annotation.Nullable; |
|||
import androidx.fragment.app.DialogFragment; |
|||
import androidx.fragment.app.Fragment; |
|||
import androidx.recyclerview.widget.GridLayoutManager; |
|||
import androidx.recyclerview.widget.RecyclerView; |
|||
|
|||
import com.google.android.material.bottomsheet.BottomSheetDialog; |
|||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment; |
|||
|
|||
import awais.instagrabber.R; |
|||
import awais.instagrabber.customviews.helpers.GridSpacingItemDecoration; |
|||
import awais.instagrabber.utils.Utils; |
|||
|
|||
public class EmojiBottomSheetDialog extends BottomSheetDialogFragment { |
|||
public static final String TAG = EmojiBottomSheetDialog.class.getSimpleName(); |
|||
|
|||
private RecyclerView grid; |
|||
private EmojiPicker.OnEmojiClickListener callback; |
|||
|
|||
@NonNull |
|||
public static EmojiBottomSheetDialog newInstance() { |
|||
// Bundle args = new Bundle(); |
|||
// fragment.setArguments(args); |
|||
return new EmojiBottomSheetDialog(); |
|||
} |
|||
|
|||
@Override |
|||
public void onCreate(@Nullable final Bundle savedInstanceState) { |
|||
super.onCreate(savedInstanceState); |
|||
setStyle(DialogFragment.STYLE_NORMAL, R.style.ThemeOverlay_Rounded_BottomSheetDialog); |
|||
} |
|||
|
|||
@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; |
|||
grid = new RecyclerView(context); |
|||
return grid; |
|||
} |
|||
|
|||
@Override |
|||
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { |
|||
init(); |
|||
} |
|||
|
|||
@Override |
|||
public void onStart() { |
|||
super.onStart(); |
|||
final Dialog dialog = getDialog(); |
|||
if (dialog == null) return; |
|||
final BottomSheetDialog bottomSheetDialog = (BottomSheetDialog) dialog; |
|||
final View bottomSheetInternal = bottomSheetDialog.findViewById(com.google.android.material.R.id.design_bottom_sheet); |
|||
if (bottomSheetInternal == null) return; |
|||
bottomSheetInternal.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT; |
|||
bottomSheetInternal.requestLayout(); |
|||
} |
|||
|
|||
@Override |
|||
public void onAttach(@NonNull final Context context) { |
|||
super.onAttach(context); |
|||
final Fragment parentFragment = getParentFragment(); |
|||
if (parentFragment instanceof EmojiPicker.OnEmojiClickListener) { |
|||
callback = (EmojiPicker.OnEmojiClickListener) parentFragment; |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void onDestroyView() { |
|||
grid = null; |
|||
super.onDestroyView(); |
|||
} |
|||
|
|||
private void init() { |
|||
final Context context = getContext(); |
|||
if (context == null) return; |
|||
final GridLayoutManager gridLayoutManager = new GridLayoutManager(context, 9); |
|||
grid.setLayoutManager(gridLayoutManager); |
|||
grid.setHasFixedSize(true); |
|||
grid.setClipToPadding(false); |
|||
grid.addItemDecoration(new GridSpacingItemDecoration(Utils.convertDpToPx(8))); |
|||
final EmojiGridAdapter adapter = new EmojiGridAdapter(null, (view, emoji) -> { |
|||
if (callback != null) { |
|||
callback.onClick(view, emoji); |
|||
} |
|||
dismiss(); |
|||
}, null); |
|||
grid.setAdapter(adapter); |
|||
} |
|||
} |
@ -1,163 +0,0 @@ |
|||
package awais.instagrabber.customviews.emoji; |
|||
|
|||
import android.content.Context; |
|||
import android.graphics.Rect; |
|||
import android.view.Gravity; |
|||
import android.view.View; |
|||
import android.view.WindowManager.LayoutParams; |
|||
import android.widget.PopupWindow; |
|||
|
|||
import awais.instagrabber.R; |
|||
import awais.instagrabber.customviews.emoji.EmojiPicker.OnBackspaceClickListener; |
|||
import awais.instagrabber.customviews.emoji.EmojiPicker.OnEmojiClickListener; |
|||
import awais.instagrabber.utils.Utils; |
|||
|
|||
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; |
|||
|
|||
/** |
|||
* https://stackoverflow.com/a/33897583/1436766 |
|||
*/ |
|||
public class EmojiPopupWindow extends PopupWindow { |
|||
|
|||
private int keyBoardHeight = 0; |
|||
private Boolean pendingOpen = false; |
|||
private Boolean isOpened = false; |
|||
private final View rootView; |
|||
private final Context context; |
|||
private final OnEmojiClickListener onEmojiClickListener; |
|||
private final OnBackspaceClickListener onBackspaceClickListener; |
|||
|
|||
private OnSoftKeyboardOpenCloseListener onSoftKeyboardOpenCloseListener; |
|||
|
|||
|
|||
/** |
|||
* Constructor |
|||
* |
|||
* @param rootView The top most layout in your view hierarchy. The difference of this view and the screen height will be used to calculate the keyboard height. |
|||
*/ |
|||
public EmojiPopupWindow(final View rootView, |
|||
final OnEmojiClickListener onEmojiClickListener, |
|||
final OnBackspaceClickListener onBackspaceClickListener) { |
|||
super(rootView.getContext()); |
|||
this.rootView = rootView; |
|||
this.context = rootView.getContext(); |
|||
this.onEmojiClickListener = onEmojiClickListener; |
|||
this.onBackspaceClickListener = onBackspaceClickListener; |
|||
View customView = createCustomView(); |
|||
setContentView(customView); |
|||
setSoftInputMode(LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); |
|||
//default size |
|||
setSize((int) context.getResources().getDimension(R.dimen.keyboard_height), MATCH_PARENT); |
|||
} |
|||
|
|||
/** |
|||
* Set the listener for the event of keyboard opening or closing. |
|||
*/ |
|||
public void setOnSoftKeyboardOpenCloseListener(OnSoftKeyboardOpenCloseListener listener) { |
|||
this.onSoftKeyboardOpenCloseListener = listener; |
|||
} |
|||
|
|||
/** |
|||
* Use this function to show the emoji popup. |
|||
* NOTE: Since, the soft keyboard sizes are variable on different android devices, the |
|||
* library needs you to open the soft keyboard atleast once before calling this function. |
|||
* If that is not possible see showAtBottomPending() function. |
|||
*/ |
|||
public void showAtBottom() { |
|||
showAtLocation(rootView, Gravity.BOTTOM, 0, 0); |
|||
} |
|||
|
|||
/** |
|||
* Use this function when the soft keyboard has not been opened yet. This |
|||
* will show the emoji popup after the keyboard is up next time. |
|||
* Generally, you will be calling InputMethodManager.showSoftInput function after |
|||
* calling this function. |
|||
*/ |
|||
public void showAtBottomPending() { |
|||
if (isKeyBoardOpen()) |
|||
showAtBottom(); |
|||
else |
|||
pendingOpen = true; |
|||
} |
|||
|
|||
/** |
|||
* @return Returns true if the soft keyboard is open, false otherwise. |
|||
*/ |
|||
public Boolean isKeyBoardOpen() { |
|||
return isOpened; |
|||
} |
|||
|
|||
/** |
|||
* Dismiss the popup |
|||
*/ |
|||
@Override |
|||
public void dismiss() { |
|||
super.dismiss(); |
|||
} |
|||
|
|||
/** |
|||
* Call this function to resize the emoji popup according to your soft keyboard size |
|||
*/ |
|||
public void setSizeForSoftKeyboard() { |
|||
rootView.getViewTreeObserver().addOnGlobalLayoutListener(() -> { |
|||
Rect r = new Rect(); |
|||
rootView.getWindowVisibleDisplayFrame(r); |
|||
|
|||
int screenHeight = getUsableScreenHeight(); |
|||
int heightDifference = screenHeight - (r.bottom - r.top); |
|||
int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android"); |
|||
if (resourceId > 0) { |
|||
heightDifference -= context.getResources() |
|||
.getDimensionPixelSize(resourceId); |
|||
} |
|||
if (heightDifference > 100) { |
|||
keyBoardHeight = heightDifference; |
|||
setSize(MATCH_PARENT, keyBoardHeight); |
|||
if (!isOpened) { |
|||
if (onSoftKeyboardOpenCloseListener != null) |
|||
onSoftKeyboardOpenCloseListener.onKeyboardOpen(keyBoardHeight); |
|||
} |
|||
isOpened = true; |
|||
if (pendingOpen) { |
|||
showAtBottom(); |
|||
pendingOpen = false; |
|||
} |
|||
} else { |
|||
isOpened = false; |
|||
if (onSoftKeyboardOpenCloseListener != null) |
|||
onSoftKeyboardOpenCloseListener.onKeyboardClose(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
private int getUsableScreenHeight() { |
|||
return Utils.displayMetrics.heightPixels; |
|||
} |
|||
|
|||
/** |
|||
* Manually set the popup window size |
|||
* |
|||
* @param width Width of the popup |
|||
* @param height Height of the popup |
|||
*/ |
|||
public void setSize(int width, int height) { |
|||
setWidth(width); |
|||
setHeight(height); |
|||
} |
|||
|
|||
private View createCustomView() { |
|||
final EmojiPicker emojiPicker = new EmojiPicker(context); |
|||
final LayoutParams layoutParams = new LayoutParams(MATCH_PARENT, MATCH_PARENT); |
|||
emojiPicker.setLayoutParams(layoutParams); |
|||
emojiPicker.init(rootView, onEmojiClickListener, onBackspaceClickListener); |
|||
return emojiPicker; |
|||
} |
|||
|
|||
|
|||
public interface OnSoftKeyboardOpenCloseListener { |
|||
void onKeyboardOpen(int keyBoardHeight); |
|||
|
|||
void onKeyboardClose(); |
|||
} |
|||
} |
|||
|
@ -0,0 +1,320 @@ |
|||
package awais.instagrabber.customviews.helpers; |
|||
|
|||
/* |
|||
* Copyright (C) 2013 The Android Open Source Project |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
|
|||
import android.animation.Animator; |
|||
import android.animation.AnimatorListenerAdapter; |
|||
import android.animation.AnimatorSet; |
|||
import android.animation.ValueAnimator; |
|||
import android.graphics.Color; |
|||
import android.util.Log; |
|||
import android.view.ViewGroup; |
|||
import android.widget.EditText; |
|||
import android.widget.TextView; |
|||
|
|||
import androidx.annotation.NonNull; |
|||
import androidx.transition.Transition; |
|||
import androidx.transition.TransitionListenerAdapter; |
|||
import androidx.transition.TransitionValues; |
|||
|
|||
import java.util.Map; |
|||
import java.util.Objects; |
|||
|
|||
import awais.instagrabber.BuildConfig; |
|||
|
|||
/** |
|||
* This transition tracks changes to the text in TextView targets. If the text |
|||
* changes between the start and end scenes, the transition ensures that the |
|||
* starting text stays until the transition ends, at which point it changes |
|||
* to the end text. This is useful in situations where you want to resize a |
|||
* text view to its new size before displaying the text that goes there. |
|||
*/ |
|||
public class ChangeText extends Transition { |
|||
private static final String LOG_TAG = "TextChange"; |
|||
private static final String PROPNAME_TEXT = "android:textchange:text"; |
|||
private static final String PROPNAME_TEXT_SELECTION_START = |
|||
"android:textchange:textSelectionStart"; |
|||
private static final String PROPNAME_TEXT_SELECTION_END = |
|||
"android:textchange:textSelectionEnd"; |
|||
private static final String PROPNAME_TEXT_COLOR = "android:textchange:textColor"; |
|||
private int mChangeBehavior = CHANGE_BEHAVIOR_KEEP; |
|||
private boolean crossFade; |
|||
/** |
|||
* Flag specifying that the text in affected/changing TextView targets will keep |
|||
* their original text during the transition, setting it to the final text when |
|||
* the transition ends. This is the default behavior. |
|||
* |
|||
* @see #setChangeBehavior(int) |
|||
*/ |
|||
public static final int CHANGE_BEHAVIOR_KEEP = 0; |
|||
/** |
|||
* Flag specifying that the text changing animation should first fade |
|||
* out the original text completely. The new text is set on the target |
|||
* view at the end of the fade-out animation. This transition is typically |
|||
* used with a later {@link #CHANGE_BEHAVIOR_IN} transition, allowing more |
|||
* flexibility than the {@link #CHANGE_BEHAVIOR_OUT_IN} by allowing other |
|||
* transitions to be run sequentially or in parallel with these fades. |
|||
* |
|||
* @see #setChangeBehavior(int) |
|||
*/ |
|||
public static final int CHANGE_BEHAVIOR_OUT = 1; |
|||
/** |
|||
* Flag specifying that the text changing animation should fade in the |
|||
* end text into the affected target view(s). This transition is typically |
|||
* used in conjunction with an earlier {@link #CHANGE_BEHAVIOR_OUT} |
|||
* transition, possibly with other transitions running as well, such as |
|||
* a sequence to fade out, then resize the view, then fade in. |
|||
* |
|||
* @see #setChangeBehavior(int) |
|||
*/ |
|||
public static final int CHANGE_BEHAVIOR_IN = 2; |
|||
/** |
|||
* Flag specifying that the text changing animation should first fade |
|||
* out the original text completely and then fade in the |
|||
* new text. |
|||
* |
|||
* @see #setChangeBehavior(int) |
|||
*/ |
|||
public static final int CHANGE_BEHAVIOR_OUT_IN = 3; |
|||
private static final String[] sTransitionProperties = { |
|||
PROPNAME_TEXT, |
|||
PROPNAME_TEXT_SELECTION_START, |
|||
PROPNAME_TEXT_SELECTION_END |
|||
}; |
|||
|
|||
/** |
|||
* Sets the type of changing animation that will be run, one of |
|||
* {@link #CHANGE_BEHAVIOR_KEEP}, {@link #CHANGE_BEHAVIOR_OUT}, |
|||
* {@link #CHANGE_BEHAVIOR_IN}, and {@link #CHANGE_BEHAVIOR_OUT_IN}. |
|||
* |
|||
* @param changeBehavior The type of fading animation to use when this |
|||
* transition is run. |
|||
* @return this textChange object. |
|||
*/ |
|||
public ChangeText setChangeBehavior(int changeBehavior) { |
|||
if (changeBehavior >= CHANGE_BEHAVIOR_KEEP && changeBehavior <= CHANGE_BEHAVIOR_OUT_IN) { |
|||
mChangeBehavior = changeBehavior; |
|||
} |
|||
return this; |
|||
} |
|||
|
|||
public ChangeText setCrossFade(final boolean crossFade) { |
|||
this.crossFade = crossFade; |
|||
return this; |
|||
} |
|||
|
|||
@Override |
|||
public String[] getTransitionProperties() { |
|||
return sTransitionProperties; |
|||
} |
|||
|
|||
/** |
|||
* Returns the type of changing animation that will be run. |
|||
* |
|||
* @return either {@link #CHANGE_BEHAVIOR_KEEP}, {@link #CHANGE_BEHAVIOR_OUT}, |
|||
* {@link #CHANGE_BEHAVIOR_IN}, or {@link #CHANGE_BEHAVIOR_OUT_IN}. |
|||
*/ |
|||
public int getChangeBehavior() { |
|||
return mChangeBehavior; |
|||
} |
|||
|
|||
private void captureValues(TransitionValues transitionValues) { |
|||
if (transitionValues.view instanceof TextView) { |
|||
TextView textview = (TextView) transitionValues.view; |
|||
transitionValues.values.put(PROPNAME_TEXT, textview.getText()); |
|||
if (textview instanceof EditText) { |
|||
transitionValues.values.put(PROPNAME_TEXT_SELECTION_START, |
|||
textview.getSelectionStart()); |
|||
transitionValues.values.put(PROPNAME_TEXT_SELECTION_END, |
|||
textview.getSelectionEnd()); |
|||
} |
|||
if (mChangeBehavior > CHANGE_BEHAVIOR_KEEP) { |
|||
transitionValues.values.put(PROPNAME_TEXT_COLOR, textview.getCurrentTextColor()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void captureStartValues(@NonNull TransitionValues transitionValues) { |
|||
captureValues(transitionValues); |
|||
} |
|||
|
|||
@Override |
|||
public void captureEndValues(@NonNull TransitionValues transitionValues) { |
|||
captureValues(transitionValues); |
|||
} |
|||
|
|||
@Override |
|||
public Animator createAnimator(@NonNull ViewGroup sceneRoot, TransitionValues startValues, |
|||
TransitionValues endValues) { |
|||
if (startValues == null || endValues == null || |
|||
!(startValues.view instanceof TextView) || !(endValues.view instanceof TextView)) { |
|||
return null; |
|||
} |
|||
final TextView view = (TextView) endValues.view; |
|||
Map<String, Object> startVals = startValues.values; |
|||
Map<String, Object> endVals = endValues.values; |
|||
final CharSequence startText = startVals.get(PROPNAME_TEXT) != null ? |
|||
(CharSequence) startVals.get(PROPNAME_TEXT) : ""; |
|||
final CharSequence endText = endVals.get(PROPNAME_TEXT) != null ? |
|||
(CharSequence) endVals.get(PROPNAME_TEXT) : ""; |
|||
final int startSelectionStart, startSelectionEnd, endSelectionStart, endSelectionEnd; |
|||
if (view instanceof EditText) { |
|||
startSelectionStart = startVals.get(PROPNAME_TEXT_SELECTION_START) != null ? |
|||
(Integer) startVals.get(PROPNAME_TEXT_SELECTION_START) : -1; |
|||
startSelectionEnd = startVals.get(PROPNAME_TEXT_SELECTION_END) != null ? |
|||
(Integer) startVals.get(PROPNAME_TEXT_SELECTION_END) : startSelectionStart; |
|||
endSelectionStart = endVals.get(PROPNAME_TEXT_SELECTION_START) != null ? |
|||
(Integer) endVals.get(PROPNAME_TEXT_SELECTION_START) : -1; |
|||
endSelectionEnd = endVals.get(PROPNAME_TEXT_SELECTION_END) != null ? |
|||
(Integer) endVals.get(PROPNAME_TEXT_SELECTION_END) : endSelectionStart; |
|||
} else { |
|||
startSelectionStart = startSelectionEnd = endSelectionStart = endSelectionEnd = -1; |
|||
} |
|||
if (!Objects.equals(startText, endText)) { |
|||
final int startColor; |
|||
final int endColor; |
|||
if (mChangeBehavior != CHANGE_BEHAVIOR_IN) { |
|||
view.setText(startText); |
|||
if (view instanceof EditText) { |
|||
setSelection(((EditText) view), startSelectionStart, startSelectionEnd); |
|||
} |
|||
} |
|||
Animator anim; |
|||
if (mChangeBehavior == CHANGE_BEHAVIOR_KEEP) { |
|||
startColor = endColor = 0; |
|||
anim = ValueAnimator.ofFloat(0, 1); |
|||
anim.addListener(new AnimatorListenerAdapter() { |
|||
@Override |
|||
public void onAnimationEnd(Animator animation) { |
|||
if (Objects.equals(startText, view.getText())) { |
|||
// Only set if it hasn't been changed since anim started |
|||
view.setText(endText); |
|||
if (view instanceof EditText) { |
|||
setSelection(((EditText) view), endSelectionStart, endSelectionEnd); |
|||
} |
|||
} |
|||
} |
|||
}); |
|||
} else { |
|||
startColor = (Integer) startVals.get(PROPNAME_TEXT_COLOR); |
|||
endColor = (Integer) endVals.get(PROPNAME_TEXT_COLOR); |
|||
// Fade out start text |
|||
ValueAnimator outAnim = null, inAnim = null; |
|||
if (mChangeBehavior == CHANGE_BEHAVIOR_OUT_IN || |
|||
mChangeBehavior == CHANGE_BEHAVIOR_OUT) { |
|||
outAnim = ValueAnimator.ofInt(Color.alpha(startColor), 0); |
|||
outAnim.addUpdateListener(animation -> { |
|||
int currAlpha = (Integer) animation.getAnimatedValue(); |
|||
view.setTextColor(currAlpha << 24 | startColor & 0xffffff); |
|||
}); |
|||
outAnim.addListener(new AnimatorListenerAdapter() { |
|||
@Override |
|||
public void onAnimationEnd(Animator animation) { |
|||
if (Objects.equals(startText, view.getText())) { |
|||
// Only set if it hasn't been changed since anim started |
|||
view.setText(endText); |
|||
if (view instanceof EditText) { |
|||
setSelection(((EditText) view), endSelectionStart, |
|||
endSelectionEnd); |
|||
} |
|||
} |
|||
// restore opaque alpha and correct end color |
|||
view.setTextColor(endColor); |
|||
} |
|||
}); |
|||
} |
|||
if (mChangeBehavior == CHANGE_BEHAVIOR_OUT_IN || |
|||
mChangeBehavior == CHANGE_BEHAVIOR_IN) { |
|||
inAnim = ValueAnimator.ofInt(0, Color.alpha(endColor)); |
|||
inAnim.addUpdateListener(animation -> { |
|||
int currAlpha = (Integer) animation.getAnimatedValue(); |
|||
view.setTextColor(currAlpha << 24 | endColor & 0xffffff); |
|||
}); |
|||
inAnim.addListener(new AnimatorListenerAdapter() { |
|||
@Override |
|||
public void onAnimationCancel(Animator animation) { |
|||
// restore opaque alpha and correct end color |
|||
view.setTextColor(endColor); |
|||
} |
|||
}); |
|||
} |
|||
if (outAnim != null && inAnim != null) { |
|||
anim = new AnimatorSet(); |
|||
final AnimatorSet animatorSet = (AnimatorSet) anim; |
|||
if (crossFade) { |
|||
animatorSet.playTogether(outAnim, inAnim); |
|||
} else { |
|||
animatorSet.playSequentially(outAnim, inAnim); |
|||
} |
|||
} else if (outAnim != null) { |
|||
anim = outAnim; |
|||
} else { |
|||
// Must be an in-only animation |
|||
anim = inAnim; |
|||
} |
|||
} |
|||
TransitionListener transitionListener = new TransitionListenerAdapter() { |
|||
int mPausedColor = 0; |
|||
|
|||
@Override |
|||
public void onTransitionPause(@NonNull Transition transition) { |
|||
if (mChangeBehavior != CHANGE_BEHAVIOR_IN) { |
|||
view.setText(endText); |
|||
if (view instanceof EditText) { |
|||
setSelection(((EditText) view), endSelectionStart, endSelectionEnd); |
|||
} |
|||
} |
|||
if (mChangeBehavior > CHANGE_BEHAVIOR_KEEP) { |
|||
mPausedColor = view.getCurrentTextColor(); |
|||
view.setTextColor(endColor); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void onTransitionResume(@NonNull Transition transition) { |
|||
if (mChangeBehavior != CHANGE_BEHAVIOR_IN) { |
|||
view.setText(startText); |
|||
if (view instanceof EditText) { |
|||
setSelection(((EditText) view), startSelectionStart, startSelectionEnd); |
|||
} |
|||
} |
|||
if (mChangeBehavior > CHANGE_BEHAVIOR_KEEP) { |
|||
view.setTextColor(mPausedColor); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void onTransitionEnd(Transition transition) { |
|||
transition.removeListener(this); |
|||
} |
|||
}; |
|||
addListener(transitionListener); |
|||
if (BuildConfig.DEBUG) { |
|||
Log.d(LOG_TAG, "createAnimator returning " + anim); |
|||
} |
|||
return anim; |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
private void setSelection(EditText editText, int start, int end) { |
|||
if (start >= 0 && end >= 0) { |
|||
editText.setSelection(start, end); |
|||
} |
|||
} |
|||
} |
@ -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<RecentSearch> 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<RecentSearch> 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(); |
|||
} |
@ -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<RecentSearch> 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); |
|||
} |
|||
} |
@ -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; |
|||
} |
|||
} |
@ -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<RecentSearch> 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<List<RecentSearch>> callback) { |
|||
// request on the I/O thread |
|||
appExecutors.diskIO().execute(() -> { |
|||
final List<RecentSearch> 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<Void> 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<Void> 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<Void> 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<Void> 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); |
|||
}); |
|||
}); |
|||
} |
|||
} |
@ -1,487 +0,0 @@ |
|||
package awais.instagrabber.fragments; |
|||
|
|||
import android.content.Context; |
|||
import android.content.DialogInterface; |
|||
import android.content.res.Resources; |
|||
import android.os.AsyncTask; |
|||
import android.os.Bundle; |
|||
import android.text.Editable; |
|||
import android.text.SpannableString; |
|||
import android.text.Spanned; |
|||
import android.text.TextWatcher; |
|||
import android.text.style.RelativeSizeSpan; |
|||
import android.util.Log; |
|||
import android.view.LayoutInflater; |
|||
import android.view.View; |
|||
import android.view.ViewGroup; |
|||
import android.view.inputmethod.InputMethodManager; |
|||
import android.widget.Toast; |
|||
|
|||
import androidx.annotation.NonNull; |
|||
import androidx.annotation.Nullable; |
|||
import androidx.appcompat.app.AlertDialog; |
|||
import androidx.appcompat.app.AppCompatActivity; |
|||
import androidx.appcompat.widget.LinearLayoutCompat; |
|||
import androidx.lifecycle.ViewModelProvider; |
|||
import androidx.navigation.NavController; |
|||
import androidx.navigation.NavDirections; |
|||
import androidx.navigation.fragment.NavHostFragment; |
|||
import androidx.recyclerview.widget.LinearLayoutManager; |
|||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; |
|||
|
|||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment; |
|||
|
|||
import java.util.Collections; |
|||
import java.util.LinkedList; |
|||
import java.util.List; |
|||
|
|||
import awais.instagrabber.BuildConfig; |
|||
import awais.instagrabber.R; |
|||
import awais.instagrabber.adapters.CommentsAdapter; |
|||
import awais.instagrabber.asyncs.CommentsFetcher; |
|||
import awais.instagrabber.customviews.helpers.RecyclerLazyLoader; |
|||
import awais.instagrabber.databinding.FragmentCommentsBinding; |
|||
import awais.instagrabber.interfaces.FetchListener; |
|||
import awais.instagrabber.models.CommentModel; |
|||
import awais.instagrabber.repositories.responses.User; |
|||
import awais.instagrabber.utils.Constants; |
|||
import awais.instagrabber.utils.CookieUtils; |
|||
import awais.instagrabber.utils.TextUtils; |
|||
import awais.instagrabber.utils.Utils; |
|||
import awais.instagrabber.viewmodels.CommentsViewModel; |
|||
import awais.instagrabber.webservices.MediaService; |
|||
import awais.instagrabber.webservices.ServiceCallback; |
|||
|
|||
import static android.content.Context.INPUT_METHOD_SERVICE; |
|||
|
|||
public final class CommentsViewerFragment extends BottomSheetDialogFragment implements SwipeRefreshLayout.OnRefreshListener { |
|||
private static final String TAG = "CommentsViewerFragment"; |
|||
|
|||
private final String cookie = Utils.settingsHelper.getString(Constants.COOKIE); |
|||
|
|||
private CommentsAdapter commentsAdapter; |
|||
private FragmentCommentsBinding binding; |
|||
private LinearLayoutManager layoutManager; |
|||
private RecyclerLazyLoader lazyLoader; |
|||
private String shortCode; |
|||
private long authorUserId, userIdFromCookie; |
|||
private String endCursor = null; |
|||
private Resources resources; |
|||
private InputMethodManager imm; |
|||
private AppCompatActivity fragmentActivity; |
|||
private LinearLayoutCompat root; |
|||
private boolean shouldRefresh = true, hasNextPage = false; |
|||
private MediaService mediaService; |
|||
private String postId; |
|||
private AsyncTask<Void, Void, List<CommentModel>> currentlyRunning; |
|||
private CommentsViewModel commentsViewModel; |
|||
|
|||
private final FetchListener<List<CommentModel>> fetchListener = new FetchListener<List<CommentModel>>() { |
|||
@Override |
|||
public void doBefore() { |
|||
binding.swipeRefreshLayout.setRefreshing(true); |
|||
} |
|||
|
|||
@Override |
|||
public void onResult(final List<CommentModel> commentModels) { |
|||
if (commentModels != null && commentModels.size() > 0) { |
|||
endCursor = commentModels.get(0).getEndCursor(); |
|||
hasNextPage = commentModels.get(0).hasNextPage(); |
|||
List<CommentModel> list = commentsViewModel.getList().getValue(); |
|||
list = list != null ? new LinkedList<>(list) : new LinkedList<>(); |
|||
// final int oldSize = list != null ? list.size() : 0; |
|||
list.addAll(commentModels); |
|||
commentsViewModel.getList().postValue(list); |
|||
} |
|||
binding.swipeRefreshLayout.setRefreshing(false); |
|||
stopCurrentExecutor(null); |
|||
} |
|||
|
|||
@Override |
|||
public void onFailure(Throwable t) { |
|||
stopCurrentExecutor(t); |
|||
} |
|||
}; |
|||
|
|||
private final CommentsAdapter.CommentCallback commentCallback = new CommentsAdapter.CommentCallback() { |
|||
@Override |
|||
public void onClick(final CommentModel comment) { |
|||
onCommentClick(comment); |
|||
} |
|||
|
|||
@Override |
|||
public void onHashtagClick(final String hashtag) { |
|||
final NavDirections action = CommentsViewerFragmentDirections.actionGlobalHashTagFragment(hashtag); |
|||
NavHostFragment.findNavController(CommentsViewerFragment.this).navigate(action); |
|||
} |
|||
|
|||
@Override |
|||
public void onMentionClick(final String mention) { |
|||
openProfile(mention); |
|||
} |
|||
|
|||
@Override |
|||
public void onURLClick(final String url) { |
|||
Utils.openURL(getContext(), url); |
|||
} |
|||
|
|||
@Override |
|||
public void onEmailClick(final String emailAddress) { |
|||
Utils.openEmailAddress(getContext(), emailAddress); |
|||
} |
|||
}; |
|||
private final View.OnClickListener newCommentListener = v -> { |
|||
final Editable text = binding.commentText.getText(); |
|||
final Context context = getContext(); |
|||
if (context == null) return; |
|||
if (text == null || TextUtils.isEmpty(text.toString())) { |
|||
Toast.makeText(context, R.string.comment_send_empty_comment, Toast.LENGTH_SHORT).show(); |
|||
return; |
|||
} |
|||
if (userIdFromCookie == 0) return; |
|||
String replyToId = null; |
|||
final CommentModel commentModel = commentsAdapter.getSelected(); |
|||
if (commentModel != null) { |
|||
replyToId = commentModel.getId(); |
|||
} |
|||
mediaService.comment(postId, text.toString(), replyToId, new ServiceCallback<Boolean>() { |
|||
@Override |
|||
public void onSuccess(final Boolean result) { |
|||
commentsAdapter.clearSelection(); |
|||
binding.commentText.setText(""); |
|||
if (!result) { |
|||
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); |
|||
return; |
|||
} |
|||
onRefresh(); |
|||
} |
|||
|
|||
@Override |
|||
public void onFailure(final Throwable t) { |
|||
Log.e(TAG, "Error during comment", t); |
|||
Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show(); |
|||
} |
|||
}); |
|||
}; |
|||
|
|||
@Override |
|||
public void onCreate(@Nullable final Bundle savedInstanceState) { |
|||
super.onCreate(savedInstanceState); |
|||
fragmentActivity = (AppCompatActivity) getActivity(); |
|||
final String deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID); |
|||
final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); |
|||
userIdFromCookie = CookieUtils.getUserIdFromCookie(cookie); |
|||
mediaService = MediaService.getInstance(deviceUuid, csrfToken, userIdFromCookie); |
|||
// setHasOptionsMenu(true); |
|||
} |
|||
|
|||
@NonNull |
|||
@Override |
|||
public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) { |
|||
if (root != null) { |
|||
shouldRefresh = false; |
|||
return root; |
|||
} |
|||
binding = FragmentCommentsBinding.inflate(getLayoutInflater()); |
|||
binding.swipeRefreshLayout.setEnabled(false); |
|||
binding.swipeRefreshLayout.setNestedScrollingEnabled(false); |
|||
root = binding.getRoot(); |
|||
return root; |
|||
} |
|||
|
|||
@Override |
|||
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { |
|||
if (!shouldRefresh) return; |
|||
init(); |
|||
shouldRefresh = false; |
|||
} |
|||
|
|||
// @Override |
|||
// public void onCreateOptionsMenu(@NonNull final Menu menu, @NonNull final MenuInflater inflater) { |
|||
// inflater.inflate(R.menu.follow, menu); |
|||
// menu.findItem(R.id.action_compare).setVisible(false); |
|||
// final MenuItem menuSearch = menu.findItem(R.id.action_search); |
|||
// final SearchView searchView = (SearchView) menuSearch.getActionView(); |
|||
// searchView.setQueryHint(getResources().getString(R.string.action_search)); |
|||
// searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { |
|||
// @Override |
|||
// public boolean onQueryTextSubmit(final String query) { |
|||
// return false; |
|||
// } |
|||
// |
|||
// @Override |
|||
// public boolean onQueryTextChange(final String query) { |
|||
// // if (commentsAdapter != null) commentsAdapter.getFilter().filter(query); |
|||
// return true; |
|||
// } |
|||
// }); |
|||
// } |
|||
|
|||
@Override |
|||
public void onRefresh() { |
|||
endCursor = null; |
|||
lazyLoader.resetState(); |
|||
commentsViewModel.getList().postValue(Collections.emptyList()); |
|||
stopCurrentExecutor(null); |
|||
currentlyRunning = new CommentsFetcher(shortCode, "", fetchListener).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); |
|||
} |
|||
|
|||
private void init() { |
|||
if (getArguments() == null) return; |
|||
final CommentsViewerFragmentArgs fragmentArgs = CommentsViewerFragmentArgs.fromBundle(getArguments()); |
|||
shortCode = fragmentArgs.getShortCode(); |
|||
postId = fragmentArgs.getPostId(); |
|||
authorUserId = fragmentArgs.getPostUserId(); |
|||
// setTitle(); |
|||
binding.swipeRefreshLayout.setOnRefreshListener(this); |
|||
binding.swipeRefreshLayout.setRefreshing(true); |
|||
commentsViewModel = new ViewModelProvider(this).get(CommentsViewModel.class); |
|||
layoutManager = new LinearLayoutManager(getContext()); |
|||
binding.rvComments.setLayoutManager(layoutManager); |
|||
commentsAdapter = new CommentsAdapter(commentCallback); |
|||
binding.rvComments.setAdapter(commentsAdapter); |
|||
commentsViewModel.getList().observe(getViewLifecycleOwner(), commentsAdapter::submitList); |
|||
resources = getResources(); |
|||
if (!TextUtils.isEmpty(cookie)) { |
|||
binding.commentField.setStartIconVisible(false); |
|||
binding.commentField.setEndIconVisible(false); |
|||
binding.commentField.setVisibility(View.VISIBLE); |
|||
binding.commentText.addTextChangedListener(new TextWatcher() { |
|||
@Override |
|||
public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) {} |
|||
|
|||
@Override |
|||
public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { |
|||
binding.commentField.setStartIconVisible(s.length() > 0); |
|||
binding.commentField.setEndIconVisible(s.length() > 0); |
|||
} |
|||
|
|||
@Override |
|||
public void afterTextChanged(final Editable s) {} |
|||
}); |
|||
binding.commentField.setStartIconOnClickListener(v -> { |
|||
commentsAdapter.clearSelection(); |
|||
binding.commentText.setText(""); |
|||
}); |
|||
binding.commentField.setEndIconOnClickListener(newCommentListener); |
|||
} |
|||
lazyLoader = new RecyclerLazyLoader(layoutManager, (page, totalItemsCount) -> { |
|||
if (hasNextPage && !TextUtils.isEmpty(endCursor)) |
|||
currentlyRunning = new CommentsFetcher(shortCode, endCursor, fetchListener).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); |
|||
endCursor = null; |
|||
}); |
|||
binding.rvComments.addOnScrollListener(lazyLoader); |
|||
stopCurrentExecutor(null); |
|||
onRefresh(); |
|||
} |
|||
|
|||
// private void setTitle() { |
|||
// final ActionBar actionBar = fragmentActivity.getSupportActionBar(); |
|||
// if (actionBar == null) return; |
|||
// actionBar.setTitle(R.string.title_comments); |
|||
// actionBar.setSubtitle(shortCode); |
|||
// } |
|||
|
|||
private void onCommentClick(final CommentModel commentModel) { |
|||
final String username = commentModel.getProfileModel().getUsername(); |
|||
final SpannableString title = new SpannableString(username + ":\n" + commentModel.getText()); |
|||
title.setSpan(new RelativeSizeSpan(1.23f), 0, username.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); |
|||
|
|||
String[] commentDialogList; |
|||
|
|||
if (!TextUtils.isEmpty(cookie) |
|||
&& userIdFromCookie != 0 |
|||
&& (userIdFromCookie == commentModel.getProfileModel().getPk() || userIdFromCookie == authorUserId)) { |
|||
commentDialogList = new String[]{ |
|||
resources.getString(R.string.open_profile), |
|||
resources.getString(R.string.comment_viewer_copy_comment), |
|||
resources.getString(R.string.comment_viewer_see_likers), |
|||
resources.getString(R.string.comment_viewer_reply_comment), |
|||
commentModel.getLiked() ? resources.getString(R.string.comment_viewer_unlike_comment) |
|||
: resources.getString(R.string.comment_viewer_like_comment), |
|||
resources.getString(R.string.comment_viewer_translate_comment), |
|||
resources.getString(R.string.comment_viewer_delete_comment) |
|||
}; |
|||
} else if (!TextUtils.isEmpty(cookie)) { |
|||
commentDialogList = new String[]{ |
|||
resources.getString(R.string.open_profile), |
|||
resources.getString(R.string.comment_viewer_copy_comment), |
|||
resources.getString(R.string.comment_viewer_see_likers), |
|||
resources.getString(R.string.comment_viewer_reply_comment), |
|||
commentModel.getLiked() ? resources.getString(R.string.comment_viewer_unlike_comment) |
|||
: resources.getString(R.string.comment_viewer_like_comment), |
|||
resources.getString(R.string.comment_viewer_translate_comment) |
|||
}; |
|||
} else { |
|||
commentDialogList = new String[]{ |
|||
resources.getString(R.string.open_profile), |
|||
resources.getString(R.string.comment_viewer_copy_comment), |
|||
resources.getString(R.string.comment_viewer_see_likers) |
|||
}; |
|||
} |
|||
final Context context = getContext(); |
|||
if (context == null) return; |
|||
final DialogInterface.OnClickListener profileDialogListener = (dialog, which) -> { |
|||
final User profileModel = commentModel.getProfileModel(); |
|||
switch (which) { |
|||
case 0: // open profile |
|||
openProfile("@" + profileModel.getUsername()); |
|||
break; |
|||
case 1: // copy comment |
|||
Utils.copyText(context, "@" + profileModel.getUsername() + ": " + commentModel.getText()); |
|||
break; |
|||
case 2: // see comment likers, this is surprisingly available to anons |
|||
final NavController navController = getNavController(); |
|||
if (navController != null) { |
|||
final Bundle bundle = new Bundle(); |
|||
bundle.putString("postId", commentModel.getId()); |
|||
bundle.putBoolean("isComment", true); |
|||
navController.navigate(R.id.action_global_likesViewerFragment, bundle); |
|||
} else Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); |
|||
break; |
|||
case 3: // reply to comment |
|||
commentsAdapter.setSelected(commentModel); |
|||
String mention = "@" + profileModel.getUsername() + " "; |
|||
binding.commentText.setText(mention); |
|||
binding.commentText.requestFocus(); |
|||
binding.commentText.setSelection(mention.length()); |
|||
binding.commentText.postDelayed(() -> { |
|||
imm = (InputMethodManager) context.getSystemService(INPUT_METHOD_SERVICE); |
|||
if (imm == null) return; |
|||
imm.showSoftInput(binding.commentText, 0); |
|||
}, 200); |
|||
break; |
|||
case 4: // like/unlike comment |
|||
if (!commentModel.getLiked()) { |
|||
mediaService.commentLike(commentModel.getId(), new ServiceCallback<Boolean>() { |
|||
@Override |
|||
public void onSuccess(final Boolean result) { |
|||
if (!result) { |
|||
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); |
|||
return; |
|||
} |
|||
commentsAdapter.setLiked(commentModel, true); |
|||
} |
|||
|
|||
@Override |
|||
public void onFailure(final Throwable t) { |
|||
Log.e(TAG, "Error liking comment", t); |
|||
try { |
|||
Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show(); |
|||
} |
|||
catch(final Throwable e) {} |
|||
} |
|||
}); |
|||
return; |
|||
} |
|||
mediaService.commentUnlike(commentModel.getId(), new ServiceCallback<Boolean>() { |
|||
@Override |
|||
public void onSuccess(final Boolean result) { |
|||
if (!result) { |
|||
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); |
|||
return; |
|||
} |
|||
commentsAdapter.setLiked(commentModel, false); |
|||
} |
|||
|
|||
@Override |
|||
public void onFailure(final Throwable t) { |
|||
Log.e(TAG, "Error unliking comment", t); |
|||
try { |
|||
Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show(); |
|||
} |
|||
catch(final Throwable e) {} |
|||
} |
|||
}); |
|||
break; |
|||
case 5: // translate comment |
|||
mediaService.translate(commentModel.getId(), "2", new ServiceCallback<String>() { |
|||
@Override |
|||
public void onSuccess(final String result) { |
|||
if (TextUtils.isEmpty(result)) { |
|||
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); |
|||
return; |
|||
} |
|||
new AlertDialog.Builder(context) |
|||
.setTitle(username) |
|||
.setMessage(result) |
|||
.setPositiveButton(R.string.ok, null) |
|||
.show(); |
|||
} |
|||
|
|||
@Override |
|||
public void onFailure(final Throwable t) { |
|||
Log.e(TAG, "Error translating comment", t); |
|||
try { |
|||
Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show(); |
|||
} |
|||
catch(final Throwable e) {} |
|||
} |
|||
}); |
|||
break; |
|||
case 6: // delete comment |
|||
if (userIdFromCookie == 0) return; |
|||
mediaService.deleteComment( |
|||
postId, commentModel.getId(), |
|||
new ServiceCallback<Boolean>() { |
|||
@Override |
|||
public void onSuccess(final Boolean result) { |
|||
if (!result) { |
|||
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); |
|||
return; |
|||
} |
|||
onRefresh(); |
|||
} |
|||
|
|||
@Override |
|||
public void onFailure(final Throwable t) { |
|||
Log.e(TAG, "Error deleting comment", t); |
|||
try { |
|||
Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show(); |
|||
} |
|||
catch(final Throwable e) {} |
|||
} |
|||
}); |
|||
break; |
|||
} |
|||
}; |
|||
new AlertDialog.Builder(context) |
|||
.setTitle(title) |
|||
.setItems(commentDialogList, profileDialogListener) |
|||
.setNegativeButton(R.string.cancel, null) |
|||
.show(); |
|||
} |
|||
|
|||
private void openProfile(final String username) { |
|||
final NavDirections action = CommentsViewerFragmentDirections.actionGlobalProfileFragment(username); |
|||
NavHostFragment.findNavController(this).navigate(action); |
|||
} |
|||
|
|||
private void stopCurrentExecutor(final Throwable t) { |
|||
if (currentlyRunning != null) { |
|||
try { |
|||
currentlyRunning.cancel(true); |
|||
} catch (final Exception e) { |
|||
if (BuildConfig.DEBUG) Log.e(TAG, "", e); |
|||
} |
|||
} |
|||
if (t != null) { |
|||
try { |
|||
Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show(); |
|||
binding.swipeRefreshLayout.setRefreshing(false); |
|||
} |
|||
catch(Throwable e) {} |
|||
} |
|||
} |
|||
|
|||
@Nullable |
|||
private NavController getNavController() { |
|||
NavController navController = null; |
|||
try { |
|||
navController = NavHostFragment.findNavController(this); |
|||
} catch (IllegalStateException e) { |
|||
Log.e(TAG, "navigateToProfile", e); |
|||
} |
|||
return navController; |
|||
} |
|||
} |
Some files were not shown because too many files changed in this diff
Write
Preview
Loading…
Cancel
Save
Reference in new issue