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"?> |
<?xml version="1.0" encoding="UTF-8"?> |
||||
<project version="4"> |
<project version="4"> |
||||
<component name="CompilerConfiguration"> |
<component name="CompilerConfiguration"> |
||||
<bytecodeTargetLevel target="1.8" /> |
|
||||
|
<bytecodeTargetLevel target="11" /> |
||||
</component> |
</component> |
||||
</project> |
</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