Merge pull request #1077 from ammargitham/feature/search-history

Search history with separate search fragment
This commit is contained in:
Austin Huang 2021-04-26 17:53:20 -04:00 committed by GitHub
commit f88bc49c30
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 2505 additions and 387 deletions

View File

@ -33,6 +33,12 @@ android {
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
compileOptions {
@ -95,6 +101,13 @@ android {
outputFileName = "barinsta_${suffix}.apk"
}
}
packagingOptions {
// Exclude file to avoid
// Error: Duplicate files during packaging of APK
exclude 'META-INF/LICENSE.md'
exclude 'META-INF/LICENSE-notice.md'
}
}
configurations.all {
@ -165,4 +178,11 @@ dependencies {
githubImplementation 'io.sentry:sentry-android:4.3.0'
testImplementation 'org.junit.jupiter:junit-jupiter:5.7.1'
androidTestImplementation 'org.junit.jupiter:junit-jupiter:5.7.1'
androidTestImplementation 'androidx.test:core:1.3.0'
androidTestImplementation 'com.android.support:support-annotations:28.0.0'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation "androidx.room:room-testing:2.2.6"
}

View File

@ -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')"
]
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -8,19 +8,18 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.provider.BaseColumns;
import android.text.Editable;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.WindowManager;
import android.widget.AutoCompleteTextView;
import android.widget.EditText;
import android.widget.Toast;
import androidx.annotation.IdRes;
@ -28,7 +27,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.SearchView;
import androidx.appcompat.widget.Toolbar;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.app.NotificationManagerCompat;
@ -50,10 +48,10 @@ import com.google.android.material.appbar.CollapsingToolbarLayout;
import com.google.android.material.badge.BadgeDrawable;
import com.google.android.material.behavior.HideBottomViewOnScrollBehavior;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.google.android.material.textfield.TextInputLayout;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterators;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.List;
@ -62,9 +60,9 @@ import java.util.stream.Collectors;
import awais.instagrabber.BuildConfig;
import awais.instagrabber.R;
import awais.instagrabber.adapters.SuggestionsAdapter;
import awais.instagrabber.asyncs.PostFetcher;
import awais.instagrabber.customviews.emoji.EmojiVariantManager;
import awais.instagrabber.customviews.helpers.TextWatcherAdapter;
import awais.instagrabber.databinding.ActivityMainBinding;
import awais.instagrabber.fragments.PostViewV2Fragment;
import awais.instagrabber.fragments.directmessages.DirectMessageInboxFragmentDirections;
@ -72,9 +70,6 @@ import awais.instagrabber.fragments.main.FeedFragment;
import awais.instagrabber.fragments.settings.PreferenceKeys;
import awais.instagrabber.models.IntentModel;
import awais.instagrabber.models.Tab;
import awais.instagrabber.models.enums.SuggestionType;
import awais.instagrabber.repositories.responses.search.SearchItem;
import awais.instagrabber.repositories.responses.search.SearchResponse;
import awais.instagrabber.services.ActivityCheckerService;
import awais.instagrabber.services.DMSyncAlarmReceiver;
import awais.instagrabber.utils.AppExecutors;
@ -88,10 +83,6 @@ import awais.instagrabber.utils.emoji.EmojiParser;
import awais.instagrabber.viewmodels.AppStateViewModel;
import awais.instagrabber.viewmodels.DirectInboxViewModel;
import awais.instagrabber.webservices.RetrofitFactory;
import awais.instagrabber.webservices.SearchService;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import static awais.instagrabber.utils.NavigationExtensions.setupWithNavController;
import static awais.instagrabber.utils.Utils.settingsHelper;
@ -100,16 +91,19 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage
private static final String TAG = "MainActivity";
private static final String FIRST_FRAGMENT_GRAPH_INDEX_KEY = "firstFragmentGraphIndex";
private static final String LAST_SELECT_NAV_MENU_ID = "lastSelectedNavMenuId";
private static final List<Integer> SEARCH_VISIBLE_DESTINATIONS = ImmutableList.of(
R.id.feedFragment,
R.id.profileFragment,
R.id.directMessagesInboxFragment,
R.id.discoverFragment,
R.id.favoritesFragment,
R.id.hashTagFragment,
R.id.locationFragment
);
private ActivityMainBinding binding;
private LiveData<NavController> currentNavControllerLiveData;
private MenuItem searchMenuItem;
private SuggestionsAdapter suggestionAdapter;
private AutoCompleteTextView searchAutoComplete;
private SearchView searchView;
private SearchService searchService;
private boolean showSearch = true;
private Handler suggestionsFetchHandler;
private int firstFragmentGraphIndex;
private int lastSelectedNavMenuId;
private boolean isActivityCheckerServiceBound = false;
@ -155,7 +149,6 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage
if (savedInstanceState == null) {
setupBottomNavigationBar(true);
}
setupSuggestions();
if (!BuildConfig.isPre) {
final boolean checkUpdates = settingsHelper.getBoolean(Constants.CHECK_UPDATES);
if (checkUpdates) FlavorTown.updateCheck(this);
@ -174,9 +167,9 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage
EmojiVariantManager.getInstance();
});
initEmojiCompat();
searchService = SearchService.getInstance();
// initDmService();
initDmUnreadCount();
initSearchInput();
}
private void setupCookie() {
@ -216,25 +209,74 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage
});
}
private void initSearchInput() {
binding.searchInputLayout.setEndIconOnClickListener(v -> {
final EditText editText = binding.searchInputLayout.getEditText();
if (editText == null) return;
editText.setText("");
});
binding.searchInputLayout.addOnEditTextAttachedListener(textInputLayout -> {
textInputLayout.setEndIconVisible(false);
final EditText editText = textInputLayout.getEditText();
if (editText == null) return;
editText.addTextChangedListener(new TextWatcherAdapter() {
@Override
public void afterTextChanged(final Editable s) {
binding.searchInputLayout.setEndIconVisible(!TextUtils.isEmpty(s));
}
});
});
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
getMenuInflater().inflate(R.menu.main_menu, menu);
searchMenuItem = menu.findItem(R.id.search);
if (showSearch && currentNavControllerLiveData != null) {
final NavController navController = currentNavControllerLiveData.getValue();
if (navController != null) {
final NavDestination currentDestination = navController.getCurrentDestination();
if (currentDestination != null) {
final int destinationId = currentDestination.getId();
showSearch = destinationId == R.id.profileFragment;
}
final NavController navController = currentNavControllerLiveData.getValue();
if (navController != null) {
final NavDestination currentDestination = navController.getCurrentDestination();
if (currentDestination != null) {
@SuppressLint("RestrictedApi") final Deque<NavBackStackEntry> backStack = navController.getBackStack();
setupMenu(backStack.size(), currentDestination.getId());
}
}
if (!showSearch) {
searchMenuItem.setVisible(false);
return true;
// if (binding.searchInputLayout.getVisibility() == View.VISIBLE) {
// searchMenuItem.setVisible(false).setEnabled(false);
// return true;
// }
// searchMenuItem.setVisible(true).setEnabled(true);
// if (showSearch && currentNavControllerLiveData != null) {
// final NavController navController = currentNavControllerLiveData.getValue();
// if (navController != null) {
// final NavDestination currentDestination = navController.getCurrentDestination();
// if (currentDestination != null) {
// final int destinationId = currentDestination.getId();
// showSearch = destinationId == R.id.profileFragment;
// }
// }
// }
// if (!showSearch) {
// searchMenuItem.setVisible(false);
// return true;
// }
// return setupSearchView();
return true;
}
@Override
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
if (item.getItemId() == R.id.search) {
final NavController navController = currentNavControllerLiveData.getValue();
if (navController == null) return false;
try {
navController.navigate(R.id.action_global_search);
return true;
} catch (Exception e) {
Log.e(TAG, "onOptionsItemSelected: ", e);
}
return false;
}
return setupSearchView();
return super.onOptionsItemSelected(item);
}
@Override
@ -336,176 +378,6 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage
notificationManager.createNotificationChannel(silentNotificationChannel);
}
private void setupSuggestions() {
suggestionsFetchHandler = new Handler();
suggestionAdapter = new SuggestionsAdapter(this, (type, query) -> {
if (searchMenuItem != null) searchMenuItem.collapseActionView();
if (searchView != null && !searchView.isIconified()) searchView.setIconified(true);
if (currentNavControllerLiveData == null) return;
final NavController navController = currentNavControllerLiveData.getValue();
if (navController == null) return;
final Bundle bundle = new Bundle();
switch (type) {
case TYPE_LOCATION:
bundle.putLong("locationId", Long.parseLong(query));
navController.navigate(R.id.action_global_locationFragment, bundle);
break;
case TYPE_HASHTAG:
bundle.putString("hashtag", query);
navController.navigate(R.id.action_global_hashTagFragment, bundle);
break;
case TYPE_USER:
bundle.putString("username", query);
navController.navigate(R.id.action_global_profileFragment, bundle);
break;
}
});
}
private boolean setupSearchView() {
final View actionView = searchMenuItem.getActionView();
if (!(actionView instanceof SearchView)) return false;
searchView = (SearchView) actionView;
searchView.setSuggestionsAdapter(suggestionAdapter);
searchView.setMaxWidth(Integer.MAX_VALUE);
final View searchText = searchView.findViewById(R.id.search_src_text);
if (searchText instanceof AutoCompleteTextView) {
searchAutoComplete = (AutoCompleteTextView) searchText;
}
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
private boolean searchUser;
private boolean searchHash;
private Call<SearchResponse> prevSuggestionAsync;
private final String[] COLUMNS = {
BaseColumns._ID,
Constants.EXTRAS_USERNAME,
Constants.EXTRAS_NAME,
Constants.EXTRAS_TYPE,
"query",
"pfp",
"verified"
};
private String currentSearchQuery;
private final Callback<SearchResponse> cb = new Callback<SearchResponse>() {
@Override
public void onResponse(@NonNull final Call<SearchResponse> call,
@NonNull final Response<SearchResponse> response) {
final MatrixCursor cursor;
final SearchResponse body = response.body();
if (body == null) {
cursor = null;
return;
}
final List<SearchItem> result = new ArrayList<>();
if (isLoggedIn) {
if (body.getList() != null) {
result.addAll(searchHash ? body.getList()
.stream()
.filter(i -> i.getUser() == null)
.collect(Collectors.toList())
: body.getList());
}
} else {
if (body.getUsers() != null && !searchHash) result.addAll(body.getUsers());
if (body.getHashtags() != null) result.addAll(body.getHashtags());
if (body.getPlaces() != null) result.addAll(body.getPlaces());
}
cursor = new MatrixCursor(COLUMNS, 0);
for (int i = 0; i < result.size(); i++) {
final SearchItem suggestionModel = result.get(i);
if (suggestionModel != null) {
Object[] objects = null;
if (suggestionModel.getUser() != null)
objects = new Object[]{
suggestionModel.getPosition(),
suggestionModel.getUser().getUsername(),
suggestionModel.getUser().getFullName(),
SuggestionType.TYPE_USER,
suggestionModel.getUser().getUsername(),
suggestionModel.getUser().getProfilePicUrl(),
suggestionModel.getUser().isVerified()};
else if (suggestionModel.getHashtag() != null)
objects = new Object[]{
suggestionModel.getPosition(),
suggestionModel.getHashtag().getName(),
suggestionModel.getHashtag().getSubtitle(),
SuggestionType.TYPE_HASHTAG,
suggestionModel.getHashtag().getName(),
"res:/" + R.drawable.ic_hashtag,
false};
else if (suggestionModel.getPlace() != null)
objects = new Object[]{
suggestionModel.getPosition(),
suggestionModel.getPlace().getTitle(),
suggestionModel.getPlace().getSubtitle(),
SuggestionType.TYPE_LOCATION,
suggestionModel.getPlace().getLocation().getPk(),
"res:/" + R.drawable.ic_location,
false};
cursor.addRow(objects);
}
}
suggestionAdapter.changeCursor(cursor);
}
@Override
public void onFailure(@NonNull final Call<SearchResponse> call,
@NonNull Throwable t) {
if (!call.isCanceled()) {
Log.e(TAG, "Exception on search:", t);
}
}
};
private final Runnable runnable = () -> {
cancelSuggestionsAsync();
if (TextUtils.isEmpty(currentSearchQuery)) {
suggestionAdapter.changeCursor(null);
return;
}
searchUser = currentSearchQuery.charAt(0) == '@';
searchHash = currentSearchQuery.charAt(0) == '#';
if (currentSearchQuery.length() == 1 && (searchHash || searchUser)) {
if (searchAutoComplete != null) {
searchAutoComplete.setThreshold(2);
}
} else {
if (searchAutoComplete != null) {
searchAutoComplete.setThreshold(1);
}
prevSuggestionAsync = searchService.search(isLoggedIn,
searchUser || searchHash ? currentSearchQuery.substring(1)
: currentSearchQuery,
searchUser ? "user" : (searchHash ? "hashtag" : "blended"));
suggestionAdapter.changeCursor(null);
prevSuggestionAsync.enqueue(cb);
}
};
private void cancelSuggestionsAsync() {
if (prevSuggestionAsync != null)
try {
prevSuggestionAsync.cancel();
} catch (final Exception ignored) {}
}
@Override
public boolean onQueryTextSubmit(final String query) {
return onQueryTextChange(query);
}
@Override
public boolean onQueryTextChange(final String query) {
suggestionsFetchHandler.removeCallbacks(runnable);
currentSearchQuery = query;
suggestionsFetchHandler.postDelayed(runnable, 800);
return true;
}
});
return true;
}
private void setupBottomNavigationBar(final boolean setDefaultTabFromSettings) {
currentTabs = !isLoggedIn ? setupAnonBottomNav() : setupMainBottomNav();
final List<Integer> mainNavList = currentTabs.stream()
@ -616,20 +488,6 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage
binding.bottomNavView.setSelectedItemId(navGraphRootId);
}
// @NonNull
// private List<Integer> getMainNavList(final int main_nav_ids) {
// final TypedArray navIds = getResources().obtainTypedArray(main_nav_ids);
// final List<Integer> mainNavList = new ArrayList<>(navIds.length());
// final int length = navIds.length();
// for (int i = 0; i < length; i++) {
// final int resourceId = navIds.getResourceId(i, -1);
// if (resourceId < 0) continue;
// mainNavList.add(resourceId);
// }
// navIds.recycle();
// return mainNavList;
// }
private void setupNavigation(final Toolbar toolbar, final NavController navController) {
if (navController == null) return;
NavigationUI.setupWithNavController(toolbar, navController);
@ -661,12 +519,10 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage
private void setupMenu(final int backStackSize, final int destinationId) {
if (searchMenuItem == null) return;
if (backStackSize >= 2 && destinationId == R.id.profileFragment) {
showSearch = true;
if (backStackSize >= 2 && SEARCH_VISIBLE_DESTINATIONS.contains(destinationId)) {
searchMenuItem.setVisible(true);
return;
}
showSearch = false;
searchMenuItem.setVisible(false);
}
@ -945,10 +801,6 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage
return currentTabs;
}
// public boolean isNavRootInCurrentTabs(@IdRes final int navRootId) {
// return showBottomViewDestinations.stream().anyMatch(id -> id == navRootId);
// }
private void setNavBarDMUnreadCountBadge(final int unseenCount) {
final BadgeDrawable badge = binding.bottomNavView.getOrCreateBadge(R.id.direct_messages_nav_graph);
if (badge == null) return;
@ -963,4 +815,14 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage
badge.setNumber(unseenCount);
badge.setVisible(true);
}
@NonNull
public TextInputLayout showSearchView() {
binding.searchInputLayout.setVisibility(View.VISIBLE);
return binding.searchInputLayout;
}
public void hideSearchView() {
binding.searchInputLayout.setVisibility(View.GONE);
}
}

View File

@ -19,7 +19,7 @@ import java.util.List;
import awais.instagrabber.R;
import awais.instagrabber.adapters.viewholder.FavoriteViewHolder;
import awais.instagrabber.databinding.ItemFavSectionHeaderBinding;
import awais.instagrabber.databinding.ItemSuggestionBinding;
import awais.instagrabber.databinding.ItemSearchResultBinding;
import awais.instagrabber.db.entities.Favorite;
import awais.instagrabber.models.enums.FavoriteType;
@ -73,7 +73,7 @@ public class FavoritesAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
// header
return new FavSectionViewHolder(ItemFavSectionHeaderBinding.inflate(inflater, parent, false));
}
final ItemSuggestionBinding binding = ItemSuggestionBinding.inflate(inflater, parent, false);
final ItemSearchResultBinding binding = ItemSearchResultBinding.inflate(inflater, parent, false);
return new FavoriteViewHolder(binding);
}

View File

@ -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();
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -6,7 +6,7 @@ import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.adapters.FavoritesAdapter;
import awais.instagrabber.databinding.ItemSuggestionBinding;
import awais.instagrabber.databinding.ItemSearchResultBinding;
import awais.instagrabber.db.entities.Favorite;
import awais.instagrabber.models.enums.FavoriteType;
import awais.instagrabber.utils.Constants;
@ -14,12 +14,12 @@ import awais.instagrabber.utils.Constants;
public class FavoriteViewHolder extends RecyclerView.ViewHolder {
private static final String TAG = "FavoriteViewHolder";
private final ItemSuggestionBinding binding;
private final ItemSearchResultBinding binding;
public FavoriteViewHolder(@NonNull final ItemSuggestionBinding binding) {
public FavoriteViewHolder(@NonNull final ItemSearchResultBinding binding) {
super(binding.getRoot());
this.binding = binding;
binding.isVerified.setVisibility(View.GONE);
binding.verified.setVisibility(View.GONE);
}
public void bind(final Favorite model,
@ -36,12 +36,12 @@ public class FavoriteViewHolder extends RecyclerView.ViewHolder {
return longClickListener.onLongClick(model);
});
if (model.getType() == FavoriteType.HASHTAG) {
binding.ivProfilePic.setImageURI(Constants.DEFAULT_HASH_TAG_PIC);
binding.profilePic.setImageURI(Constants.DEFAULT_HASH_TAG_PIC);
} else {
binding.ivProfilePic.setImageURI(model.getPicUrl());
binding.profilePic.setImageURI(model.getPicUrl());
}
binding.tvFullName.setText(model.getDisplayName());
binding.tvUsername.setVisibility(View.VISIBLE);
binding.title.setVisibility(View.VISIBLE);
binding.subtitle.setText(model.getDisplayName());
String query = model.getQuery();
switch (model.getType()) {
case HASHTAG:
@ -51,11 +51,11 @@ public class FavoriteViewHolder extends RecyclerView.ViewHolder {
query = "@" + query;
break;
case LOCATION:
binding.tvUsername.setVisibility(View.GONE);
binding.title.setVisibility(View.GONE);
break;
default:
// do nothing
}
binding.tvUsername.setText(query);
binding.title.setText(query);
}
}

View File

@ -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);
}
}

View File

@ -22,14 +22,16 @@ import java.util.List;
import awais.instagrabber.db.dao.AccountDao;
import awais.instagrabber.db.dao.DMLastNotifiedDao;
import awais.instagrabber.db.dao.FavoriteDao;
import awais.instagrabber.db.dao.RecentSearchDao;
import awais.instagrabber.db.entities.Account;
import awais.instagrabber.db.entities.DMLastNotified;
import awais.instagrabber.db.entities.Favorite;
import awais.instagrabber.db.entities.RecentSearch;
import awais.instagrabber.models.enums.FavoriteType;
import awais.instagrabber.utils.Utils;
@Database(entities = {Account.class, Favorite.class, DMLastNotified.class},
version = 5)
@Database(entities = {Account.class, Favorite.class, DMLastNotified.class, RecentSearch.class},
version = 6)
@TypeConverters({Converters.class})
public abstract class AppDatabase extends RoomDatabase {
private static final String TAG = AppDatabase.class.getSimpleName();
@ -42,12 +44,14 @@ public abstract class AppDatabase extends RoomDatabase {
public abstract DMLastNotifiedDao dmLastNotifiedDao();
public abstract RecentSearchDao recentSearchDao();
public static AppDatabase getDatabase(final Context context) {
if (INSTANCE == null) {
synchronized (AppDatabase.class) {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, "cookiebox.db")
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5)
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6)
.build();
}
}
@ -156,6 +160,21 @@ public abstract class AppDatabase extends RoomDatabase {
}
};
static final Migration MIGRATION_5_6 = new Migration(5, 6) {
@Override
public void migrate(@NonNull final SupportSQLiteDatabase database) {
database.execSQL("CREATE TABLE IF NOT EXISTS `recent_searches` (" +
"`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " +
"`ig_id` TEXT NOT NULL, " +
"`name` TEXT NOT NULL, " +
"`username` TEXT, " +
"`pic_url` TEXT, " +
"`type` TEXT NOT NULL, " +
"`last_searched_on` INTEGER NOT NULL)");
database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_recent_searches_ig_id_type` ON `recent_searches` (`ig_id`, `type`)");
}
};
@NonNull
private static List<Favorite> backupOldFavorites(@NonNull final SupportSQLiteDatabase db) {
// check if old favorites table had the column query_display

View File

@ -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();
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
});
});
}
}

View File

@ -41,7 +41,9 @@ public class FavoritesFragment extends Fragment {
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
favoriteRepository = FavoriteRepository.getInstance(FavoriteDataSource.getInstance(getContext()));
final Context context = getContext();
if (context == null) return;
favoriteRepository = FavoriteRepository.getInstance(FavoriteDataSource.getInstance(context));
}
@NonNull

View File

@ -0,0 +1,197 @@
package awais.instagrabber.fragments.search;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import java.io.Serializable;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import awais.instagrabber.adapters.SearchItemsAdapter;
import awais.instagrabber.models.Resource;
import awais.instagrabber.models.enums.FavoriteType;
import awais.instagrabber.repositories.responses.search.SearchItem;
import awais.instagrabber.viewmodels.SearchFragmentViewModel;
public class SearchCategoryFragment extends Fragment {
private static final String TAG = SearchCategoryFragment.class.getSimpleName();
private static final String ARG_TYPE = "type";
@Nullable
private SwipeRefreshLayout swipeRefreshLayout;
@Nullable
private RecyclerView list;
private SearchFragmentViewModel viewModel;
private FavoriteType type;
private SearchItemsAdapter searchItemsAdapter;
@Nullable
private OnSearchItemClickListener onSearchItemClickListener;
private boolean skipViewRefresh;
private String prevQuery;
@NonNull
public static SearchCategoryFragment newInstance(@NonNull final FavoriteType type) {
final SearchCategoryFragment fragment = new SearchCategoryFragment();
final Bundle args = new Bundle();
args.putSerializable(ARG_TYPE, type);
fragment.setArguments(args);
return fragment;
}
public SearchCategoryFragment() {}
@Override
public void onAttach(@NonNull final Context context) {
super.onAttach(context);
final Fragment parentFragment = getParentFragment();
if (!(parentFragment instanceof OnSearchItemClickListener)) return;
onSearchItemClickListener = (OnSearchItemClickListener) parentFragment;
}
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final FragmentActivity fragmentActivity = getActivity();
if (fragmentActivity == null) return;
viewModel = new ViewModelProvider(fragmentActivity).get(SearchFragmentViewModel.class);
final Bundle args = getArguments();
if (args == null) {
Log.e(TAG, "onCreate: arguments are null");
return;
}
final Serializable typeSerializable = args.getSerializable(ARG_TYPE);
if (!(typeSerializable instanceof FavoriteType)) {
Log.e(TAG, "onCreate: type not a FavoriteType");
return;
}
type = (FavoriteType) typeSerializable;
}
@Nullable
@Override
public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) {
final Context context = getContext();
if (context == null) return null;
skipViewRefresh = false;
if (swipeRefreshLayout != null) {
skipViewRefresh = true;
return swipeRefreshLayout;
}
swipeRefreshLayout = new SwipeRefreshLayout(context);
swipeRefreshLayout.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
list = new RecyclerView(context);
list.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
swipeRefreshLayout.addView(list);
return swipeRefreshLayout;
}
@Override
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
if (skipViewRefresh) return;
setupList();
}
@Override
public void onResume() {
super.onResume();
// Log.d(TAG, "onResume: type: " + type);
setupObservers();
final String currentQuery = viewModel.getQuery().getValue();
if (prevQuery != null && currentQuery != null && !Objects.equals(prevQuery, currentQuery)) {
viewModel.search(currentQuery, type);
}
prevQuery = null;
}
private void setupList() {
if (list == null || swipeRefreshLayout == null) return;
final Context context = getContext();
if (context == null) return;
list.setLayoutManager(new LinearLayoutManager(context));
searchItemsAdapter = new SearchItemsAdapter(onSearchItemClickListener);
list.setAdapter(searchItemsAdapter);
swipeRefreshLayout.setOnRefreshListener(() -> {
String currentQuery = viewModel.getQuery().getValue();
if (currentQuery == null) currentQuery = "";
viewModel.search(currentQuery, type);
});
}
private void setupObservers() {
viewModel.getQuery().observe(getViewLifecycleOwner(), q -> {
if (!isVisible() || Objects.equals(prevQuery, q)) return;
viewModel.search(q, type);
prevQuery = q;
});
final LiveData<Resource<List<SearchItem>>> resultsLiveData = getResultsLiveData();
if (resultsLiveData != null) {
resultsLiveData.observe(getViewLifecycleOwner(), this::onResults);
}
}
private void onResults(final Resource<List<SearchItem>> listResource) {
if (listResource == null) return;
switch (listResource.status) {
case SUCCESS:
if (searchItemsAdapter != null) {
searchItemsAdapter.submitList(listResource.data);
}
if (swipeRefreshLayout != null) {
swipeRefreshLayout.setRefreshing(false);
}
break;
case ERROR:
if (searchItemsAdapter != null) {
searchItemsAdapter.submitList(Collections.emptyList());
}
if (swipeRefreshLayout != null) {
swipeRefreshLayout.setRefreshing(false);
}
break;
case LOADING:
if (swipeRefreshLayout != null) {
swipeRefreshLayout.setRefreshing(true);
}
break;
default:
break;
}
}
@Nullable
private LiveData<Resource<List<SearchItem>>> getResultsLiveData() {
switch (type) {
case TOP:
return viewModel.getTopResults();
case USER:
return viewModel.getUserResults();
case HASHTAG:
return viewModel.getHashtagResults();
case LOCATION:
return viewModel.getLocationResults();
}
return null;
}
public interface OnSearchItemClickListener {
void onSearchItemClick(SearchItem searchItem);
void onSearchItemDelete(SearchItem searchItem);
}
}

View File

@ -0,0 +1,248 @@
package awais.instagrabber.fragments.search;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.LinearLayoutCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavController;
import androidx.navigation.fragment.NavHostFragment;
import com.google.android.material.snackbar.Snackbar;
import com.google.android.material.tabs.TabLayoutMediator;
import com.google.android.material.textfield.TextInputLayout;
import java.util.Arrays;
import java.util.List;
import awais.instagrabber.R;
import awais.instagrabber.activities.MainActivity;
import awais.instagrabber.adapters.SearchCategoryAdapter;
import awais.instagrabber.customviews.helpers.TextWatcherAdapter;
import awais.instagrabber.databinding.FragmentSearchBinding;
import awais.instagrabber.models.Resource;
import awais.instagrabber.models.enums.FavoriteType;
import awais.instagrabber.repositories.responses.search.SearchItem;
import awais.instagrabber.viewmodels.SearchFragmentViewModel;
public class SearchFragment extends Fragment implements SearchCategoryFragment.OnSearchItemClickListener {
private static final String TAG = SearchFragment.class.getSimpleName();
private static final String QUERY = "query";
private FragmentSearchBinding binding;
private LinearLayoutCompat root;
private boolean shouldRefresh = true;
@Nullable
private TextInputLayout searchInputLayout;
@Nullable
private EditText searchInput;
@Nullable
private MainActivity mainActivity;
private SearchFragmentViewModel viewModel;
private final TextWatcherAdapter textWatcher = new TextWatcherAdapter() {
@Override
public void afterTextChanged(final Editable s) {
if (s == null) return;
viewModel.submitQuery(s.toString().trim());
}
};
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final FragmentActivity fragmentActivity = getActivity();
if (!(fragmentActivity instanceof MainActivity)) return;
mainActivity = (MainActivity) fragmentActivity;
viewModel = new ViewModelProvider(mainActivity).get(SearchFragmentViewModel.class);
}
@Nullable
@Override
public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) {
if (root != null) {
shouldRefresh = false;
return root;
}
binding = FragmentSearchBinding.inflate(inflater, container, false);
root = binding.getRoot();
return root;
}
@Override
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
if (!shouldRefresh) return;
init(savedInstanceState);
shouldRefresh = false;
}
@Override
public void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState);
final String current = viewModel.getQuery().getValue();
if (TextUtils.isEmpty(current)) return;
outState.putString(QUERY, current);
}
@Override
public void onPause() {
super.onPause();
if (mainActivity != null) {
mainActivity.hideSearchView();
}
}
@Override
public void onDestroy() {
super.onDestroy();
if (mainActivity != null) {
mainActivity.hideSearchView();
}
if (searchInput != null) {
searchInput.removeTextChangedListener(textWatcher);
searchInput.setText("");
}
}
@Override
public void onResume() {
super.onResume();
if (mainActivity != null) {
mainActivity.showSearchView();
}
if (searchInputLayout != null) {
searchInputLayout.requestFocus();
}
}
private void init(@Nullable final Bundle savedInstanceState) {
if (mainActivity == null) return;
searchInputLayout = mainActivity.showSearchView();
searchInput = searchInputLayout.getEditText();
setupObservers();
setupViewPager();
setupSearchInput(savedInstanceState);
}
private void setupObservers() {
viewModel.getQuery().observe(getViewLifecycleOwner(), q -> {}); // need to observe, so that getQuery returns proper value
}
private void setupSearchInput(@Nullable final Bundle savedInstanceState) {
if (searchInput == null) return;
searchInput.removeTextChangedListener(textWatcher); // make sure we add only 1 instance of textWatcher
searchInput.addTextChangedListener(textWatcher);
boolean triggerEmptyQuery = true;
if (savedInstanceState != null) {
final String savedQuery = savedInstanceState.getString(QUERY);
if (TextUtils.isEmpty(savedQuery)) return;
searchInput.setText(savedQuery);
triggerEmptyQuery = false;
}
searchInput.requestFocus();
if (triggerEmptyQuery) {
viewModel.submitQuery("");
}
}
private void setupViewPager() {
binding.pager.setSaveEnabled(false);
final List<FavoriteType> categories = Arrays.asList(FavoriteType.values());
binding.pager.setAdapter(new SearchCategoryAdapter(this, categories));
final TabLayoutMediator mediator = new TabLayoutMediator(binding.tabLayout, binding.pager, (tab, position) -> {
try {
final FavoriteType type = categories.get(position);
final int resId;
switch (type) {
case TOP:
resId = R.string.top;
break;
case USER:
resId = R.string.accounts;
break;
case HASHTAG:
resId = R.string.hashtags;
break;
case LOCATION:
resId = R.string.locations;
break;
default:
throw new IllegalStateException("Unexpected value: " + type);
}
tab.setText(resId);
} catch (Exception e) {
Log.e(TAG, "setupViewPager: ", e);
}
});
mediator.attach();
}
@Override
public void onSearchItemClick(final SearchItem searchItem) {
if (searchItem == null) return;
final FavoriteType type = searchItem.getType();
if (type == null) return;
try {
if (!searchItem.isFavorite()) {
viewModel.saveToRecentSearches(searchItem); // insert or update recent
}
final NavController navController = NavHostFragment.findNavController(this);
final Bundle bundle = new Bundle();
switch (type) {
case USER:
bundle.putString("username", searchItem.getUser().getUsername());
navController.navigate(R.id.action_global_profileFragment, bundle);
break;
case HASHTAG:
bundle.putString("hashtag", searchItem.getHashtag().getName());
navController.navigate(R.id.action_global_hashTagFragment, bundle);
break;
case LOCATION:
bundle.putLong("locationId", searchItem.getPlace().getLocation().getPk());
navController.navigate(R.id.action_global_locationFragment, bundle);
break;
default:
break;
}
} catch (Exception e) {
Log.e(TAG, "onSearchItemClick: ", e);
}
}
@Override
public void onSearchItemDelete(final SearchItem searchItem) {
final LiveData<Resource<Object>> liveData = viewModel.deleteRecentSearch(searchItem);
if (liveData == null) return;
liveData.observe(getViewLifecycleOwner(), new Observer<Resource<Object>>() {
@Override
public void onChanged(final Resource<Object> resource) {
if (resource == null) return;
switch (resource.status) {
case SUCCESS:
viewModel.search("", FavoriteType.TOP);
liveData.removeObserver(this);
break;
case ERROR:
Snackbar.make(binding.getRoot(), R.string.error, Snackbar.LENGTH_SHORT);
liveData.removeObserver(this);
break;
case LOADING:
default:
break;
}
}
});
}
}

View File

@ -1,7 +1,8 @@
package awais.instagrabber.models.enums;
public enum FavoriteType {
TOP, // used just for searching
USER,
HASHTAG,
LOCATION
LOCATION,
}

View File

@ -1,6 +1,7 @@
package awais.instagrabber.repositories.responses;
import java.io.Serializable;
import java.util.Objects;
import awais.instagrabber.models.enums.FollowingType;
@ -42,4 +43,21 @@ public final class Hashtag implements Serializable {
public String getSubtitle() {
return searchResultSubtitle;
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final Hashtag hashtag = (Hashtag) o;
return mediaCount == hashtag.mediaCount &&
following == hashtag.following &&
Objects.equals(id, hashtag.id) &&
Objects.equals(name, hashtag.name) &&
Objects.equals(searchResultSubtitle, hashtag.searchResultSubtitle);
}
@Override
public int hashCode() {
return Objects.hash(following, mediaCount, id, name, searchResultSubtitle);
}
}

View File

@ -1,5 +1,7 @@
package awais.instagrabber.repositories.responses;
import java.util.Objects;
public class Place {
private final Location location;
// for search
@ -40,4 +42,21 @@ public class Place {
public String getStatus() {
return status;
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final Place place = (Place) o;
return Objects.equals(location, place.location) &&
Objects.equals(title, place.title) &&
Objects.equals(subtitle, place.subtitle) &&
Objects.equals(slug, place.slug) &&
Objects.equals(status, place.status);
}
@Override
public int hashCode() {
return Objects.hash(location, title, subtitle, slug, status);
}
}

View File

@ -1,15 +1,34 @@
package awais.instagrabber.repositories.responses.search;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import awais.instagrabber.db.entities.Favorite;
import awais.instagrabber.db.entities.RecentSearch;
import awais.instagrabber.models.enums.FavoriteType;
import awais.instagrabber.repositories.responses.Hashtag;
import awais.instagrabber.repositories.responses.Location;
import awais.instagrabber.repositories.responses.Place;
import awais.instagrabber.repositories.responses.User;
public class SearchItem {
private static final String TAG = SearchItem.class.getSimpleName();
private final User user;
private final Place place;
private final Hashtag hashtag;
private final int position;
private boolean isRecent = false;
private boolean isFavorite = false;
public SearchItem(final User user,
final Place place,
final Hashtag hashtag,
@ -35,4 +54,221 @@ public class SearchItem {
public int getPosition() {
return position;
}
public boolean isRecent() {
return isRecent;
}
public void setRecent(final boolean recent) {
isRecent = recent;
}
public boolean isFavorite() {
return isFavorite;
}
public void setFavorite(final boolean favorite) {
isFavorite = favorite;
}
@Nullable
public FavoriteType getType() {
if (user != null) {
return FavoriteType.USER;
}
if (hashtag != null) {
return FavoriteType.HASHTAG;
}
if (place != null) {
return FavoriteType.LOCATION;
}
return null;
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final SearchItem that = (SearchItem) o;
return Objects.equals(user, that.user) &&
Objects.equals(place, that.place) &&
Objects.equals(hashtag, that.hashtag);
}
@Override
public int hashCode() {
return Objects.hash(user, place, hashtag);
}
@NonNull
@Override
public String toString() {
return "SearchItem{" +
"user=" + user +
", place=" + place +
", hashtag=" + hashtag +
", position=" + position +
", isRecent=" + isRecent +
'}';
}
@NonNull
public static List<SearchItem> fromRecentSearch(final List<RecentSearch> recentSearches) {
if (recentSearches == null) return Collections.emptyList();
return recentSearches.stream()
.map(SearchItem::fromRecentSearch)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
@Nullable
private static SearchItem fromRecentSearch(final RecentSearch recentSearch) {
if (recentSearch == null) return null;
try {
final FavoriteType type = recentSearch.getType();
final SearchItem searchItem;
switch (type) {
case USER:
searchItem = new SearchItem(getUser(recentSearch), null, null, 0);
break;
case HASHTAG:
searchItem = new SearchItem(null, null, getHashtag(recentSearch), 0);
break;
case LOCATION:
searchItem = new SearchItem(null, getPlace(recentSearch), null, 0);
break;
default:
return null;
}
searchItem.setRecent(true);
return searchItem;
} catch (Exception e) {
Log.e(TAG, "fromRecentSearch: ", e);
}
return null;
}
public static List<SearchItem> fromFavorite(final List<Favorite> favorites) {
if (favorites == null) {
return Collections.emptyList();
}
return favorites.stream()
.map(SearchItem::fromFavorite)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
@Nullable
private static SearchItem fromFavorite(final Favorite favorite) {
if (favorite == null) return null;
final FavoriteType type = favorite.getType();
if (type == null) return null;
final SearchItem searchItem;
switch (type) {
case USER:
searchItem = new SearchItem(getUser(favorite), null, null, 0);
break;
case HASHTAG:
searchItem = new SearchItem(null, null, getHashtag(favorite), 0);
break;
case LOCATION:
final Place place = getPlace(favorite);
if (place == null) return null;
searchItem = new SearchItem(null, place, null, 0);
break;
default:
return null;
}
searchItem.setFavorite(true);
return searchItem;
}
@NonNull
private static User getUser(@NonNull final RecentSearch recentSearch) {
return new User(
Long.parseLong(recentSearch.getIgId()),
recentSearch.getUsername(),
recentSearch.getName(),
false,
recentSearch.getPicUrl(),
null, null, false, false, false, false, false,
null, null, 0, 0, 0, 0, null, null,
0, null, null, null, null, null, null
);
}
@NonNull
private static User getUser(@NonNull final Favorite favorite) {
return new User(
0,
favorite.getQuery(),
favorite.getDisplayName(),
false,
favorite.getPicUrl(),
null, null, false, false, false, false, false,
null, null, 0, 0, 0, 0, null, null,
0, null, null, null, null, null, null
);
}
@NonNull
private static Hashtag getHashtag(@NonNull final RecentSearch recentSearch) {
return new Hashtag(
recentSearch.getIgId(),
recentSearch.getName(),
0,
null,
null
);
}
@NonNull
private static Hashtag getHashtag(@NonNull final Favorite favorite) {
return new Hashtag(
"0",
favorite.getQuery(),
0,
null,
null
);
}
@NonNull
private static Place getPlace(@NonNull final RecentSearch recentSearch) {
final Location location = new Location(
Long.parseLong(recentSearch.getIgId()),
recentSearch.getName(),
recentSearch.getName(),
null, null, 0, 0
);
return new Place(
location,
recentSearch.getName(),
null,
null,
null
);
}
@Nullable
private static Place getPlace(@NonNull final Favorite favorite) {
try {
final Location location = new Location(
Long.parseLong(favorite.getQuery()),
favorite.getDisplayName(),
favorite.getDisplayName(),
null, null, 0, 0
);
return new Place(
location,
favorite.getDisplayName(),
null,
null,
null
);
} catch (Exception e) {
Log.e(TAG, "getPlace: ", e);
return null;
}
}
}

View File

@ -8,8 +8,6 @@ import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import awais.instagrabber.db.datasources.AccountDataSource;
import awais.instagrabber.db.repositories.AccountRepository;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils;
@ -23,20 +21,18 @@ public class AppStateViewModel extends AndroidViewModel {
private static final String TAG = AppStateViewModel.class.getSimpleName();
private final String cookie;
private final boolean isLoggedIn;
private final MutableLiveData<User> currentUser = new MutableLiveData<>();
private AccountRepository accountRepository;
private UserService userService;
public AppStateViewModel(@NonNull final Application application) {
super(application);
// Log.d(TAG, "AppStateViewModel: constructor");
cookie = settingsHelper.getString(Constants.COOKIE);
isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) > 0;
final boolean isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) != 0;
if (!isLoggedIn) return;
userService = UserService.getInstance();
accountRepository = AccountRepository.getInstance(AccountDataSource.getInstance(application));
// final AccountRepository accountRepository = AccountRepository.getInstance(AccountDataSource.getInstance(application));
fetchProfileDetails();
}
@ -50,6 +46,7 @@ public class AppStateViewModel extends AndroidViewModel {
private void fetchProfileDetails() {
final long uid = CookieUtils.getUserIdFromCookie(cookie);
if (userService == null) return;
userService.getUserInfo(uid, new ServiceCallback<User>() {
@Override
public void onSuccess(final User user) {

View File

@ -0,0 +1,352 @@
package awais.instagrabber.viewmodels;
import android.app.Application;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import awais.instagrabber.db.datasources.FavoriteDataSource;
import awais.instagrabber.db.datasources.RecentSearchDataSource;
import awais.instagrabber.db.entities.Favorite;
import awais.instagrabber.db.entities.RecentSearch;
import awais.instagrabber.db.repositories.FavoriteRepository;
import awais.instagrabber.db.repositories.RecentSearchRepository;
import awais.instagrabber.db.repositories.RepositoryCallback;
import awais.instagrabber.models.Resource;
import awais.instagrabber.models.enums.FavoriteType;
import awais.instagrabber.repositories.responses.search.SearchItem;
import awais.instagrabber.repositories.responses.search.SearchResponse;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.Debouncer;
import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.webservices.SearchService;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import static androidx.lifecycle.Transformations.distinctUntilChanged;
import static awais.instagrabber.utils.Utils.settingsHelper;
public class SearchFragmentViewModel extends AppStateViewModel {
private static final String TAG = SearchFragmentViewModel.class.getSimpleName();
private static final String QUERY = "query";
private final MutableLiveData<String> query = new MutableLiveData<>();
private final MutableLiveData<Resource<List<SearchItem>>> topResults = new MutableLiveData<>();
private final MutableLiveData<Resource<List<SearchItem>>> userResults = new MutableLiveData<>();
private final MutableLiveData<Resource<List<SearchItem>>> hashtagResults = new MutableLiveData<>();
private final MutableLiveData<Resource<List<SearchItem>>> locationResults = new MutableLiveData<>();
private final SearchService searchService;
private final Debouncer<String> searchDebouncer;
private final boolean isLoggedIn;
private final LiveData<String> distinctQuery;
private final RecentSearchRepository recentSearchRepository;
private final FavoriteRepository favoriteRepository;
private String tempQuery;
public SearchFragmentViewModel(@NonNull final Application application) {
super(application);
final String cookie = settingsHelper.getString(Constants.COOKIE);
isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) != 0;
final Debouncer.Callback<String> searchCallback = new Debouncer.Callback<String>() {
@Override
public void call(final String key) {
if (tempQuery == null) return;
query.postValue(tempQuery);
}
@Override
public void onError(final Throwable t) {
Log.e(TAG, "onError: ", t);
}
};
searchDebouncer = new Debouncer<>(searchCallback, 500);
distinctQuery = distinctUntilChanged(query);
searchService = SearchService.getInstance();
recentSearchRepository = RecentSearchRepository.getInstance(RecentSearchDataSource.getInstance(application));
favoriteRepository = FavoriteRepository.getInstance(FavoriteDataSource.getInstance(application));
}
public LiveData<String> getQuery() {
return distinctQuery;
}
public LiveData<Resource<List<SearchItem>>> getTopResults() {
return topResults;
}
public LiveData<Resource<List<SearchItem>>> getUserResults() {
return userResults;
}
public LiveData<Resource<List<SearchItem>>> getHashtagResults() {
return hashtagResults;
}
public LiveData<Resource<List<SearchItem>>> getLocationResults() {
return locationResults;
}
public void submitQuery(@Nullable final String query) {
String localQuery = query;
if (query == null) {
localQuery = "";
}
if (tempQuery != null && Objects.equals(localQuery.toLowerCase(), tempQuery.toLowerCase())) return;
tempQuery = query;
if (TextUtils.isEmpty(query)) {
// If empty immediately post it
searchDebouncer.cancel(QUERY);
this.query.postValue("");
return;
}
searchDebouncer.call(QUERY);
}
public void search(@NonNull final String query,
@NonNull final FavoriteType type) {
final MutableLiveData<Resource<List<SearchItem>>> liveData = getLiveDataByType(type);
if (liveData == null) return;
if (TextUtils.isEmpty(query)) {
if (type != FavoriteType.TOP) {
liveData.postValue(Resource.success(Collections.emptyList()));
return;
}
showRecentSearchesAndFavorites();
return;
}
if (query.equals("@") || query.equals("#")) return;
final String c;
switch (type) {
case TOP:
c = "blended";
break;
case USER:
c = "user";
break;
case HASHTAG:
c = "hashtag";
break;
case LOCATION:
c = "place";
break;
default:
return;
}
liveData.postValue(Resource.loading(null));
final Call<SearchResponse> request = searchService.search(isLoggedIn, query, c);
request.enqueue(new Callback<SearchResponse>() {
@Override
public void onResponse(@NonNull final Call<SearchResponse> call,
@NonNull final Response<SearchResponse> response) {
if (!response.isSuccessful()) {
sendErrorResponse(type);
return;
}
final SearchResponse body = response.body();
if (body == null) {
sendErrorResponse(type);
return;
}
parseResponse(body, type);
}
@Override
public void onFailure(@NonNull final Call<SearchResponse> call,
@NonNull final Throwable t) {
Log.e(TAG, "onFailure: ", t);
}
});
}
private void showRecentSearchesAndFavorites() {
final SettableFuture<List<RecentSearch>> recentResultsFuture = SettableFuture.create();
final SettableFuture<List<Favorite>> favoritesFuture = SettableFuture.create();
recentSearchRepository.getAllRecentSearches(new RepositoryCallback<List<RecentSearch>>() {
@Override
public void onSuccess(final List<RecentSearch> result) {
recentResultsFuture.set(result);
}
@Override
public void onDataNotAvailable() {
recentResultsFuture.set(Collections.emptyList());
}
});
favoriteRepository.getAllFavorites(new RepositoryCallback<List<Favorite>>() {
@Override
public void onSuccess(final List<Favorite> result) {
favoritesFuture.set(result);
}
@Override
public void onDataNotAvailable() {
favoritesFuture.set(Collections.emptyList());
}
});
//noinspection UnstableApiUsage
final ListenableFuture<List<List<?>>> listenableFuture = Futures.allAsList(recentResultsFuture, favoritesFuture);
Futures.addCallback(listenableFuture, new FutureCallback<List<List<?>>>() {
@Override
public void onSuccess(@Nullable final List<List<?>> result) {
if (!TextUtils.isEmpty(tempQuery)) return; // Make sure user has not entered anything before updating results
if (result == null) {
topResults.postValue(Resource.success(Collections.emptyList()));
return;
}
try {
//noinspection unchecked
topResults.postValue(Resource.success(
ImmutableList.<SearchItem>builder()
.addAll(SearchItem.fromRecentSearch((List<RecentSearch>) result.get(0)))
.addAll(SearchItem.fromFavorite((List<Favorite>) result.get(1)))
.build()
));
} catch (Exception e) {
Log.e(TAG, "onSuccess: ", e);
topResults.postValue(Resource.success(Collections.emptyList()));
}
}
@Override
public void onFailure(@NonNull final Throwable t) {
if (!TextUtils.isEmpty(tempQuery)) return;
topResults.postValue(Resource.success(Collections.emptyList()));
Log.e(TAG, "onFailure: ", t);
}
}, AppExecutors.getInstance().mainThread());
}
private void sendErrorResponse(@NonNull final FavoriteType type) {
final MutableLiveData<Resource<List<SearchItem>>> liveData = getLiveDataByType(type);
if (liveData == null) return;
liveData.postValue(Resource.error(null, Collections.emptyList()));
}
private MutableLiveData<Resource<List<SearchItem>>> getLiveDataByType(@NonNull final FavoriteType type) {
final MutableLiveData<Resource<List<SearchItem>>> liveData;
switch (type) {
case TOP:
liveData = topResults;
break;
case USER:
liveData = userResults;
break;
case HASHTAG:
liveData = hashtagResults;
break;
case LOCATION:
liveData = locationResults;
break;
default:
return null;
}
return liveData;
}
private void parseResponse(@NonNull final SearchResponse body,
@NonNull final FavoriteType type) {
final MutableLiveData<Resource<List<SearchItem>>> liveData = getLiveDataByType(type);
if (liveData == null) return;
if (isLoggedIn) {
if (body.getList() == null) {
liveData.postValue(Resource.success(Collections.emptyList()));
return;
}
if (type == FavoriteType.HASHTAG || type == FavoriteType.LOCATION) {
liveData.postValue(Resource.success(body.getList()
.stream()
.filter(i -> i.getUser() == null)
.collect(Collectors.toList())));
return;
}
liveData.postValue(Resource.success(body.getList()));
return;
}
// anonymous
final List<SearchItem> list;
switch (type) {
case TOP:
list = ImmutableList
.<SearchItem>builder()
.addAll(body.getUsers())
.addAll(body.getHashtags())
.addAll(body.getPlaces())
.build();
break;
case USER:
list = body.getUsers();
break;
case HASHTAG:
list = body.getHashtags();
break;
case LOCATION:
list = body.getPlaces();
break;
default:
return;
}
liveData.postValue(Resource.success(list));
}
public void saveToRecentSearches(final SearchItem searchItem) {
if (searchItem == null) return;
try {
final RecentSearch recentSearch = RecentSearch.fromSearchItem(searchItem);
if (recentSearch == null) return;
recentSearchRepository.insertOrUpdateRecentSearch(recentSearch, new RepositoryCallback<Void>() {
@Override
public void onSuccess(final Void result) {
// Log.d(TAG, "onSuccess: inserted recent: " + recentSearch);
}
@Override
public void onDataNotAvailable() {}
});
} catch (Exception e) {
Log.e(TAG, "saveToRecentSearches: ", e);
}
}
@Nullable
public LiveData<Resource<Object>> deleteRecentSearch(final SearchItem searchItem) {
if (searchItem == null || !searchItem.isRecent()) return null;
final RecentSearch recentSearch = RecentSearch.fromSearchItem(searchItem);
if (recentSearch == null) return null;
final MutableLiveData<Resource<Object>> data = new MutableLiveData<>();
data.postValue(Resource.loading(null));
recentSearchRepository.deleteRecentSearchByIgIdAndType(recentSearch.getIgId(), recentSearch.getType(), new RepositoryCallback<Void>() {
@Override
public void onSuccess(final Void result) {
// Log.d(TAG, "onSuccess: deleted");
data.postValue(Resource.success(new Object()));
}
@Override
public void onDataNotAvailable() {
// Log.e(TAG, "onDataNotAvailable: not deleted");
data.postValue(Resource.error("Error deleting recent item", null));
}
});
return data;
}
}

View File

@ -29,10 +29,32 @@
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorSurface"
android:background="?attr/toolbarColor"
app:layout_collapseMode="pin"
app:title="@string/app_name"
tools:menu="@menu/main_menu" />
tools:menu="@menu/main_menu">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/search_input_layout"
style="?searchInputStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
app:boxStrokeColor="@null"
app:boxStrokeWidth="0dp"
app:boxStrokeWidthFocused="0dp"
app:endIconContentDescription="@string/clear"
app:endIconDrawable="@drawable/ic_close_24"
app:endIconMode="custom"
app:hintEnabled="false"
tools:visibility="visible">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/search" />
</com.google.android.material.textfield.TextInputLayout>
</com.google.android.material.appbar.MaterialToolbar>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>

View File

@ -4,4 +4,4 @@
android:id="@+id/favorite_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/item_suggestion" />
tools:listitem="@layout/item_search_result" />

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
style="@style/Widget.MaterialComponents.TabLayout.RegularCaps"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</androidx.appcompat.widget.LinearLayoutCompat>

View File

@ -13,7 +13,7 @@
android:paddingBottom="8dp">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/ivProfilePic"
android:id="@+id/profile_pic"
android:layout_width="40dp"
android:layout_height="40dp"
app:actualImageScaleType="centerCrop"
@ -23,46 +23,61 @@
tools:background="@mipmap/ic_launcher" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tvUsername"
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:gravity="center_vertical"
android:maxLines="1"
android:paddingStart="16dp"
android:paddingEnd="4dp"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
app:layout_constraintBottom_toTopOf="@id/tvFullName"
app:layout_constraintEnd_toStartOf="@id/isVerified"
app:layout_constraintStart_toEndOf="@id/ivProfilePic"
app:layout_constraintBottom_toTopOf="@id/subtitle"
app:layout_constraintEnd_toStartOf="@id/verified"
app:layout_constraintStart_toEndOf="@id/profile_pic"
app:layout_constraintTop_toTopOf="parent"
tools:text="username" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tvFullName"
android:id="@+id/subtitle"
android:layout_width="0dp"
android:layout_height="0dp"
android:gravity="center_vertical"
android:maxLines="1"
android:paddingStart="16dp"
android:paddingEnd="0dp"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/ivProfilePic"
app:layout_constraintTop_toBottomOf="@id/tvUsername"
app:layout_constraintEnd_toStartOf="@id/delete"
app:layout_constraintStart_toEndOf="@id/profile_pic"
app:layout_constraintTop_toBottomOf="@id/title"
tools:text="full name"
tools:visibility="visible" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/isVerified"
android:id="@+id/verified"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_gravity="end"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
android:visibility="gone"
app:layout_constraintBaseline_toBaselineOf="@id/tvUsername"
app:layout_constraintBottom_toTopOf="@id/tvFullName"
app:layout_constraintStart_toEndOf="@id/tvUsername"
app:layout_constraintBaseline_toBaselineOf="@id/title"
app:layout_constraintBottom_toTopOf="@id/subtitle"
app:layout_constraintStart_toEndOf="@id/title"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/verified"
tools:visibility="visible" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/delete"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:background="?selectableItemBackgroundBorderless"
android:padding="8dp"
android:src="@drawable/ic_close_24"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,25 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<!--<item-->
<!-- android:id="@+id/favourites"-->
<!-- android:enabled="true"-->
<!-- android:icon="@drawable/ic_star_24"-->
<!-- android:title="@string/title_favorites"-->
<!-- app:showAsAction="ifRoom" />-->
<!--<item-->
<!-- android:id="@+id/direct_messages"-->
<!-- android:enabled="true"-->
<!-- android:icon="@drawable/ic_send_24"-->
<!-- android:title="@string/action_dms"-->
<!-- app:showAsAction="always" />-->
<item
android:id="@+id/search"
android:enabled="true"
android:icon="@drawable/ic_search_24"
android:title="@string/action_search"
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:showAsAction="always|collapseActionView" />
android:title="@string/search"
app:showAsAction="always" />
</menu>

View File

@ -96,6 +96,10 @@
android:id="@+id/action_global_user_search"
app:destination="@id/user_search_nav_graph" />
<action
android:id="@+id/action_global_search"
app:destination="@id/searchFragment" />
<fragment
android:id="@+id/directMessagesInboxFragment"
android:name="awais.instagrabber.fragments.directmessages.DirectMessageInboxFragment"
@ -187,4 +191,10 @@
android:id="@+id/action_pending_inbox_to_thread"
app:destination="@id/directMessagesThreadFragment" />
</fragment>
<fragment
android:id="@+id/searchFragment"
android:name="awais.instagrabber.fragments.search.SearchFragment"
android:label="@string/search"
tools:layout="@layout/fragment_search" />
</navigation>

View File

@ -95,6 +95,10 @@
app:argType="long" />
</action>
<action
android:id="@+id/action_global_search"
app:destination="@id/searchFragment" />
<fragment
android:id="@+id/discoverFragment"
android:name="awais.instagrabber.fragments.main.DiscoverFragment"
@ -120,4 +124,9 @@
android:name="backgroundColor"
app:argType="integer" />
</fragment>
<fragment
android:id="@+id/searchFragment"
android:name="awais.instagrabber.fragments.search.SearchFragment"
android:label="@string/search"
tools:layout="@layout/fragment_search" />
</navigation>

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/favorites_nav_graph"
app:startDestination="@id/favoritesFragment">
@ -36,8 +37,18 @@
app:argType="long" />
</action>
<action
android:id="@+id/action_global_search"
app:destination="@id/searchFragment" />
<fragment
android:id="@+id/favoritesFragment"
android:name="awais.instagrabber.fragments.FavoritesFragment"
android:label="@string/title_favorites" />
android:label="@string/title_favorites"
tools:layout="@layout/fragment_favorites" />
<fragment
android:id="@+id/searchFragment"
android:name="awais.instagrabber.fragments.search.SearchFragment"
android:label="@string/search"
tools:layout="@layout/fragment_search" />
</navigation>

View File

@ -106,6 +106,10 @@
app:nullable="false" />
</action>
<action
android:id="@+id/action_global_search"
app:destination="@id/searchFragment" />
<fragment
android:id="@+id/feedFragment"
android:name="awais.instagrabber.fragments.main.FeedFragment"
@ -115,7 +119,6 @@
android:id="@+id/action_feedFragment_to_storyViewerFragment"
app:destination="@id/storyViewerFragment" />
</fragment>
<fragment
android:id="@+id/storyViewerFragment"
android:name="awais.instagrabber.fragments.StoryViewerFragment"
@ -125,4 +128,9 @@
android:name="options"
app:argType="awais.instagrabber.repositories.requests.StoryViewerOptions" />
</fragment>
<fragment
android:id="@+id/searchFragment"
android:name="awais.instagrabber.fragments.search.SearchFragment"
android:label="@string/search"
tools:layout="@layout/fragment_search" />
</navigation>

View File

@ -65,6 +65,10 @@
app:argType="long" />
</action>
<action
android:id="@+id/action_global_search"
app:destination="@id/searchFragment" />
<fragment
android:id="@+id/hashTagFragment"
android:name="awais.instagrabber.fragments.HashTagFragment"
@ -87,6 +91,11 @@
android:name="options"
app:argType="awais.instagrabber.repositories.requests.StoryViewerOptions" />
</fragment>
<fragment
android:id="@+id/searchFragment"
android:name="awais.instagrabber.fragments.search.SearchFragment"
android:label="@string/search"
tools:layout="@layout/fragment_search" />
<action
android:id="@+id/action_global_hashTagFragment"
app:destination="@id/hashTagFragment">

View File

@ -66,6 +66,10 @@
app:nullable="false" />
</action>
<action
android:id="@+id/action_global_search"
app:destination="@id/searchFragment" />
<fragment
android:id="@+id/locationFragment"
android:name="awais.instagrabber.fragments.LocationFragment"
@ -94,4 +98,9 @@
android:name="options"
app:argType="awais.instagrabber.repositories.requests.StoryViewerOptions" />
</fragment>
<fragment
android:id="@+id/searchFragment"
android:name="awais.instagrabber.fragments.search.SearchFragment"
android:label="@string/search"
tools:layout="@layout/fragment_search" />
</navigation>

View File

@ -112,6 +112,10 @@
app:argType="boolean" />
</action>
<action
android:id="@+id/action_global_search"
app:destination="@id/searchFragment" />
<fragment
android:id="@+id/profileFragment"
android:name="awais.instagrabber.fragments.main.ProfileFragment"
@ -188,32 +192,9 @@
android:name="options"
app:argType="awais.instagrabber.repositories.requests.StoryViewerOptions" />
</fragment>
<!--<fragment-->
<!-- android:id="@+id/directMessagesThreadFragment"-->
<!-- android:name="awais.instagrabber.fragments.directmessages.DirectMessageThreadFragment"-->
<!-- android:label="DirectMessagesThreadFragment"-->
<!-- tools:layout="@layout/fragment_direct_messages_thread">-->
<!-- <argument-->
<!-- android:name="threadId"-->
<!-- app:argType="string" />-->
<!-- <argument-->
<!-- android:name="title"-->
<!-- app:argType="string" />-->
<!-- <action-->
<!-- android:id="@+id/action_dMThreadFragment_to_dMSettingsFragment"-->
<!-- app:destination="@id/directMessagesSettingsFragment" />-->
<!--</fragment>-->
<!--<fragment-->
<!-- android:id="@+id/directMessagesSettingsFragment"-->
<!-- android:name="awais.instagrabber.fragments.directmessages.DirectMessageSettingsFragment"-->
<!-- android:label="@string/details"-->
<!-- tools:layout="@layout/fragment_direct_messages_settings">-->
<!-- <argument-->
<!-- android:name="threadId"-->
<!-- app:argType="string"-->
<!-- app:nullable="false"/>-->
<!-- <argument-->
<!-- android:name="title"-->
<!-- app:argType="string" />-->
<!--</fragment>-->
<fragment
android:id="@+id/searchFragment"
android:name="awais.instagrabber.fragments.search.SearchFragment"
android:label="@string/search"
tools:layout="@layout/fragment_search" />
</navigation>

View File

@ -20,6 +20,9 @@
<attr name="dmWaveformProgressColor" format="reference" />
<attr name="dmInputTextColor" format="reference" />
<attr name="toolbarColor" format="reference" />
<attr name="searchInputStyle" format="reference" />
<declare-styleable name="RecordView">
<attr name="slide_to_cancel_text" format="string" />
<attr name="slide_to_cancel_text_color" format="reference" />

View File

@ -495,4 +495,7 @@
<string name="dm_remove_warning">If saved, all DM related features will be disabled on next launch</string>
<string name="copy_caption">Copy caption</string>
<string name="copy_reply">Copy reply</string>
<string name="top">Top</string>
<string name="recent">Recent</string>
<string name="clear">Clear</string>
</resources>

View File

@ -225,4 +225,39 @@
<item name="android:paddingEnd">0dp</item>
<item name="iconPadding">0dp</item>
</style>
<style name="TextAppearance.Design.Tab.RegularCaps" parent="TextAppearance.Design.Tab">
<item name="textAllCaps">false</item>
<item name="android:textAllCaps">false</item>
</style>
<style name="Widget.MaterialComponents.TabLayout.RegularCaps" parent="Widget.MaterialComponents.TabLayout">
<item name="tabTextAppearance">@style/TextAppearance.Design.Tab.RegularCaps</item>
</style>
<style name="Widget.MaterialComponents.TabLayout.Light.White" parent="Widget.MaterialComponents.TabLayout">
<item name="materialThemeOverlay">@style/ThemeOverlay.App.TabLayout.Light.White</item>
</style>
<style name="ThemeOverlay.App.TabLayout.Light.White" parent="">
<item name="colorPrimary">@color/black</item>
</style>
<style name="Widget.MaterialComponents.TabLayout.Dark.Black" parent="Widget.MaterialComponents.TabLayout">
<item name="materialThemeOverlay">@style/ThemeOverlay.App.TabLayout.Dark.Black</item>
</style>
<style name="ThemeOverlay.App.TabLayout.Dark.Black" parent="">
<item name="colorPrimary">@color/white</item>
</style>
<style name="Widget.MaterialComponents.TextInputLayout.OutlinedBox.Light.Barinsta" parent="Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<item name="materialThemeOverlay">@style/ThemeOverlay.MaterialComponents.TextInputEditText.OutlinedBox.Light.Barinsta</item>
<item name="colorPrimary">@color/black</item>
<item name="colorOnSurface">@color/black</item>
</style>
<style name="ThemeOverlay.MaterialComponents.TextInputEditText.OutlinedBox.Light.Barinsta" parent="ThemeOverlay.MaterialComponents.TextInputEditText.OutlinedBox">
<item name="colorPrimary">@color/white</item>
</style>
</resources>

View File

@ -23,6 +23,7 @@
<item name="dmIncomingBgColor">@color/grey_600</item>
<item name="dmOutgoingBgColor">@color/deep_purple_400</item>
<item name="dmDateHeaderBgColor">@color/deep_purple_600</item>
<item name="searchInputStyle">@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox</item>
</style>
<style name="AppTheme.Light.White" parent="AppTheme.Light">
@ -33,8 +34,11 @@
<item name="colorSecondaryVariant">@color/white</item>
<item name="colorOnSecondary">@color/black</item>
<item name="colorSurface">@color/white</item>
<item name="toolbarColor">?colorSurface</item>
<item name="colorOnSurface">@color/black</item>
<item name="colorAccent">@color/black</item>
<item name="colorControlActivated">@color/black</item>
<item name="android:colorControlActivated">@color/black</item>
<item name="editTextColor">@color/black</item>
<item name="android:editTextColor">@color/black</item>
<item name="android:textColorPrimary">@color/black</item>
@ -48,6 +52,7 @@
<item name="dmWaveformBgColor">@color/grey_600</item>
<item name="dmWaveformProgressColor">@color/blue_800</item>
<item name="dmInputTextColor">@color/black</item>
<item name="tabStyle">@style/Widget.MaterialComponents.TabLayout.Light.White</item>
</style>
<style name="AppTheme.Light.Barinsta" parent="AppTheme.Light">
@ -57,6 +62,7 @@
<item name="colorSecondary">@color/barinstaColorSecondary</item>
<item name="colorSecondaryVariant">@color/barinstaColorSecondaryDark</item>
<item name="colorSurface">@color/grey_200</item>
<item name="toolbarColor">?colorPrimary</item>
<item name="colorControlHighlight">@color/barinstaColorSecondaryDark</item>
<item name="colorAccent">@color/barinstaColorSecondaryDark</item>
<!--<item name="actionBarTheme">@style/ThemeOverlay.MaterialComponents.ActionBar</item>-->
@ -65,6 +71,7 @@
<item name="android:textColorPrimary">@color/barinstaPrimaryTextColor</item>
<item name="toolbarStyle">@style/Widget.MaterialComponents.Toolbar.Light.Barinsta</item>
<item name="dmInputTextColor">@color/black</item>
<item name="searchInputStyle">@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Light.Barinsta</item>
</style>
<style name="AppTheme.Light.Bibliogram" parent="AppTheme.Light">
@ -74,6 +81,7 @@
<item name="colorSecondary">@color/bibliogramColorSecondary</item>
<item name="colorSecondaryVariant">@color/bibliogramColorSecondaryDark</item>
<item name="colorSurface">@color/grey_200</item>
<item name="toolbarColor">?colorPrimary</item>
<item name="colorControlHighlight">@color/bibliogramColorSecondaryDark</item>
<item name="colorAccent">@color/bibliogramColorSecondaryDark</item>
<item name="android:windowBackground">?colorSurface</item>
@ -81,6 +89,7 @@
<item name="android:textColorPrimary">@color/bibliogramPrimaryTextColor</item>
<item name="toolbarStyle">@style/Widget.MaterialComponents.Toolbar.Light.Barinsta</item>
<item name="dmInputTextColor">@color/black</item>
<item name="searchInputStyle">@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Light.Barinsta</item>
</style>
@ -103,6 +112,7 @@
<item name="dmWaveformBgColor">@color/white</item>
<item name="dmWaveformProgressColor">@color/blue_800</item>
<item name="dmInputTextColor">@color/white</item>
<item name="searchInputStyle">@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox</item>
</style>
<style name="AppTheme.Dark.Black" parent="AppTheme.Dark">
@ -114,9 +124,12 @@
<item name="colorOnSecondary">@color/white</item>
<item name="colorSecondaryVariant">@color/black</item>
<item name="colorSurface">@color/black</item>
<item name="toolbarColor">?colorSurface</item>
<item name="colorOnSurface">@color/white</item>
<item name="colorAccent">@color/blue_A700</item>
<item name="colorControlHighlight">@color/grey_600</item>
<item name="colorControlActivated">@color/white</item>
<item name="android:colorControlActivated">@color/white</item>
<item name="android:textColorPrimary">@color/white</item>
<item name="android:textColorSecondary">@color/white</item>
<item name="android:textColorHint">@color/grey_500</item>
@ -132,6 +145,7 @@
<item name="dmIncomingBgColor">@color/grey_600</item>
<item name="dmOutgoingBgColor">@color/deep_purple_400</item>
<item name="dmDateHeaderBgColor">@color/deep_purple_600</item>
<item name="tabStyle">@style/Widget.MaterialComponents.TabLayout.Dark.Black</item>
</style>
<style name="AppTheme.Dark.MaterialDark" parent="AppTheme.Dark">
@ -142,6 +156,7 @@
<item name="android:windowBackground">@color/grey_900</item>
<item name="android:colorBackground">@color/grey_900</item>
<item name="colorSurface">@color/grey_900</item>
<item name="toolbarColor">?colorSurface</item>
<item name="colorError">@color/red_200</item>
<item name="colorOnPrimary">@color/black</item>
<item name="colorOnSecondary">@color/black</item>