Merge branch 'master' into support-android-11
5
.codebeatsettings
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"JAVA": {
|
||||
"TOO_MANY_IVARS": [8, 10, 20, 30]
|
||||
}
|
||||
}
|
@ -20,8 +20,8 @@ android {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 30
|
||||
|
||||
versionCode 61
|
||||
versionName '19.2.0'
|
||||
versionCode 62
|
||||
versionName '19.2.1'
|
||||
|
||||
multiDexEnabled true
|
||||
|
||||
@ -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 {
|
||||
@ -106,7 +119,7 @@ dependencies {
|
||||
|
||||
def appcompat_version = "1.2.0"
|
||||
def nav_version = '2.3.4'
|
||||
def exoplayer_version = '2.13.2'
|
||||
def exoplayer_version = '2.13.3'
|
||||
|
||||
implementation 'com.google.android.material:material:1.4.0-alpha02'
|
||||
|
||||
@ -139,15 +152,13 @@ dependencies {
|
||||
def camerax_version = "1.1.0-alpha03"
|
||||
implementation "androidx.camera:camera-camera2:$camerax_version"
|
||||
implementation "androidx.camera:camera-lifecycle:$camerax_version"
|
||||
implementation "androidx.camera:camera-view:1.0.0-alpha22"
|
||||
implementation "androidx.camera:camera-view:1.0.0-alpha24"
|
||||
|
||||
// EmojiCompat
|
||||
def emoji_compat_version = "1.1.0"
|
||||
implementation "androidx.emoji:emoji:$emoji_compat_version"
|
||||
implementation "androidx.emoji:emoji-appcompat:$emoji_compat_version"
|
||||
|
||||
implementation 'me.austinhuang:AutoLinkTextViewV2:-SNAPSHOT'
|
||||
|
||||
implementation 'com.facebook.fresco:fresco:2.3.0'
|
||||
implementation 'com.facebook.fresco:animated-webp:2.3.0'
|
||||
implementation 'com.facebook.fresco:webpsupport:2.3.0'
|
||||
@ -157,6 +168,8 @@ dependencies {
|
||||
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
|
||||
|
||||
implementation 'org.apache.commons:commons-imaging:1.0-alpha2'
|
||||
|
||||
implementation 'com.github.ammargitham:AutoLinkTextViewV2:v3.1.0'
|
||||
implementation 'com.github.ammargitham:uCrop:2.3-beta'
|
||||
implementation 'com.github.ammargitham:android-gpuimage:2.1.1-beta4'
|
||||
|
||||
@ -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"
|
||||
|
||||
}
|
||||
|
227
app/schemas/awais.instagrabber.db.AppDatabase/6.json
Normal 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')"
|
||||
]
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
package awais.instagrabber.db;
|
||||
|
||||
import androidx.room.Room;
|
||||
import androidx.room.migration.Migration;
|
||||
import androidx.room.testing.MigrationTestHelper;
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static awais.instagrabber.db.AppDatabase.MIGRATION_4_5;
|
||||
import static awais.instagrabber.db.AppDatabase.MIGRATION_5_6;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class MigrationTest {
|
||||
private static final String TEST_DB = "migration-test";
|
||||
private static final Migration[] ALL_MIGRATIONS = new Migration[]{MIGRATION_4_5, MIGRATION_5_6};
|
||||
|
||||
@Rule
|
||||
public MigrationTestHelper helper;
|
||||
|
||||
public MigrationTest() {
|
||||
final String canonicalName = AppDatabase.class.getCanonicalName();
|
||||
assert canonicalName != null;
|
||||
helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(),
|
||||
canonicalName,
|
||||
new FrameworkSQLiteOpenHelperFactory());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void migrateAll() throws IOException {
|
||||
// Create earliest version of the database. Have to start with 4 since that is the version we migrated to Room.
|
||||
SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 4);
|
||||
db.close();
|
||||
|
||||
// Open latest version of the database. Room will validate the schema
|
||||
// once all migrations execute.
|
||||
AppDatabase appDb = Room.databaseBuilder(InstrumentationRegistry.getInstrumentation().getTargetContext(),
|
||||
AppDatabase.class,
|
||||
TEST_DB)
|
||||
.addMigrations(ALL_MIGRATIONS).build();
|
||||
appDb.getOpenHelper().getWritableDatabase();
|
||||
appDb.close();
|
||||
}
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
package awais.instagrabber.db.dao;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.room.Room;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import awais.instagrabber.db.AppDatabase;
|
||||
import awais.instagrabber.db.entities.RecentSearch;
|
||||
import awais.instagrabber.models.enums.FavoriteType;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class RecentSearchDaoTest {
|
||||
private static final String TAG = RecentSearchDaoTest.class.getSimpleName();
|
||||
|
||||
private RecentSearchDao dao;
|
||||
private AppDatabase db;
|
||||
|
||||
@Before
|
||||
public void createDb() {
|
||||
final Context context = ApplicationProvider.getApplicationContext();
|
||||
db = Room.inMemoryDatabaseBuilder(context, AppDatabase.class).build();
|
||||
dao = db.recentSearchDao();
|
||||
}
|
||||
|
||||
@After
|
||||
public void closeDb() {
|
||||
db.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writeQueryDelete() {
|
||||
final RecentSearch recentSearch = insertRecentSearch("1", "test1", FavoriteType.HASHTAG);
|
||||
final RecentSearch byIgIdAndType = dao.getRecentSearchByIgIdAndType("1", FavoriteType.HASHTAG);
|
||||
Assertions.assertEquals(recentSearch, byIgIdAndType);
|
||||
dao.deleteRecentSearch(byIgIdAndType);
|
||||
final RecentSearch deleted = dao.getRecentSearchByIgIdAndType("1", FavoriteType.HASHTAG);
|
||||
Assertions.assertNull(deleted);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queryAllOrdered() {
|
||||
final List<RecentSearch> insertListReversed = ImmutableList
|
||||
.<RecentSearch>builder()
|
||||
.add(insertRecentSearch("1", "test1", FavoriteType.HASHTAG))
|
||||
.add(insertRecentSearch("2", "test2", FavoriteType.LOCATION))
|
||||
.add(insertRecentSearch("3", "test3", FavoriteType.USER))
|
||||
.add(insertRecentSearch("4", "test4", FavoriteType.USER))
|
||||
.add(insertRecentSearch("5", "test5", FavoriteType.USER))
|
||||
.build()
|
||||
.reverse(); // important
|
||||
final List<RecentSearch> fromDb = dao.getAllRecentSearches();
|
||||
Assertions.assertIterableEquals(insertListReversed, fromDb);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private RecentSearch insertRecentSearch(final String igId, final String name, final FavoriteType type) {
|
||||
final RecentSearch recentSearch = new RecentSearch(
|
||||
igId,
|
||||
name,
|
||||
null,
|
||||
null,
|
||||
type,
|
||||
LocalDateTime.now()
|
||||
);
|
||||
dao.insertRecentSearch(recentSearch);
|
||||
return recentSearch;
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="enable_sentry">Włącz Sentry</string>
|
||||
<string name="sentry_summary">Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io</string>
|
||||
<string name="sentry_summary">Sentry jest słuchaczem/obsługą błędów, które asynchronicznie wysyłają błąd/zdarzenie do Sentry.io</string>
|
||||
<string name="sentry_start_next_launch">Sentry rozpocznie się przy następnym uruchomieniu</string>
|
||||
</resources>
|
||||
|
@ -53,6 +53,8 @@
|
||||
<data android:scheme="https" />
|
||||
<data android:host="ig.me" />
|
||||
<data android:host="www.ig.me" />
|
||||
<data android:host="instagr.am" />
|
||||
<data android:host="www.instagr.am" />
|
||||
<data android:host="instagram.com" />
|
||||
<data android:host="www.instagram.com" />
|
||||
<data android:pathPrefix="/" />
|
||||
@ -173,4 +175,4 @@
|
||||
android:value="Noto Color Emoji Compat" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
@ -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;
|
||||
@ -89,10 +84,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.Constants.EXTRA_INITIAL_URI;
|
||||
import static awais.instagrabber.utils.NavigationExtensions.setupWithNavController;
|
||||
@ -102,16 +93,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;
|
||||
@ -167,7 +161,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);
|
||||
@ -186,9 +179,9 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage
|
||||
EmojiVariantManager.getInstance();
|
||||
});
|
||||
initEmojiCompat();
|
||||
searchService = SearchService.getInstance();
|
||||
// initDmService();
|
||||
initDmUnreadCount();
|
||||
initSearchInput();
|
||||
}
|
||||
|
||||
private void setupCookie() {
|
||||
@ -228,25 +221,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
|
||||
@ -353,176 +395,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()
|
||||
@ -581,6 +453,13 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage
|
||||
|
||||
private List<Tab> setupAnonBottomNav() {
|
||||
final int selectedItemId = binding.bottomNavView.getSelectedItemId();
|
||||
final Tab favoriteTab = new Tab(R.drawable.ic_star_24,
|
||||
getString(R.string.title_favorites),
|
||||
false,
|
||||
"favorites_nav_graph",
|
||||
R.navigation.favorites_nav_graph,
|
||||
R.id.favorites_nav_graph,
|
||||
R.id.favoritesFragment);
|
||||
final Tab profileTab = new Tab(R.drawable.ic_person_24,
|
||||
getString(R.string.profile),
|
||||
false,
|
||||
@ -597,12 +476,15 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage
|
||||
R.id.morePreferencesFragment);
|
||||
final Menu menu = binding.bottomNavView.getMenu();
|
||||
menu.clear();
|
||||
menu.add(0, favoriteTab.getNavigationRootId(), 0, favoriteTab.getTitle()).setIcon(favoriteTab.getIconResId());
|
||||
menu.add(0, profileTab.getNavigationRootId(), 0, profileTab.getTitle()).setIcon(profileTab.getIconResId());
|
||||
menu.add(0, moreTab.getNavigationRootId(), 0, moreTab.getTitle()).setIcon(moreTab.getIconResId());
|
||||
if (selectedItemId != R.id.profile_nav_graph && selectedItemId != R.id.more_nav_graph) {
|
||||
if (selectedItemId != R.id.profile_nav_graph
|
||||
&& selectedItemId != R.id.more_nav_graph
|
||||
&& selectedItemId != R.id.favorites_nav_graph) {
|
||||
setBottomNavSelectedTab(profileTab);
|
||||
}
|
||||
return ImmutableList.of(profileTab, moreTab);
|
||||
return ImmutableList.of(favoriteTab, profileTab, moreTab);
|
||||
}
|
||||
|
||||
private List<Tab> setupMainBottomNav() {
|
||||
@ -623,20 +505,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);
|
||||
@ -668,12 +536,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);
|
||||
}
|
||||
|
||||
@ -952,10 +818,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;
|
||||
@ -970,4 +832,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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,33 @@
|
||||
package awais.instagrabber.adapters;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import awais.instagrabber.fragments.search.SearchCategoryFragment;
|
||||
import awais.instagrabber.models.enums.FavoriteType;
|
||||
|
||||
public class SearchCategoryAdapter extends FragmentStateAdapter {
|
||||
|
||||
private final List<FavoriteType> categories;
|
||||
|
||||
public SearchCategoryAdapter(@NonNull final Fragment fragment,
|
||||
@NonNull final List<FavoriteType> categories) {
|
||||
super(fragment);
|
||||
this.categories = categories;
|
||||
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Fragment createFragment(final int position) {
|
||||
return SearchCategoryFragment.newInstance(categories.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return categories.size();
|
||||
}
|
||||
}
|
@ -0,0 +1,215 @@
|
||||
package awais.instagrabber.adapters;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.AdapterListUpdateCallback;
|
||||
import androidx.recyclerview.widget.AsyncDifferConfig;
|
||||
import androidx.recyclerview.widget.AsyncListDiffer;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import awais.instagrabber.R;
|
||||
import awais.instagrabber.adapters.viewholder.SearchItemViewHolder;
|
||||
import awais.instagrabber.databinding.ItemFavSectionHeaderBinding;
|
||||
import awais.instagrabber.databinding.ItemSearchResultBinding;
|
||||
import awais.instagrabber.fragments.search.SearchCategoryFragment.OnSearchItemClickListener;
|
||||
import awais.instagrabber.models.enums.FavoriteType;
|
||||
import awais.instagrabber.repositories.responses.search.SearchItem;
|
||||
|
||||
public final class SearchItemsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||
private static final String TAG = SearchItemsAdapter.class.getSimpleName();
|
||||
private static final DiffUtil.ItemCallback<SearchItemOrHeader> DIFF_CALLBACK = new DiffUtil.ItemCallback<SearchItemOrHeader>() {
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull final SearchItemOrHeader oldItem, @NonNull final SearchItemOrHeader newItem) {
|
||||
return Objects.equals(oldItem, newItem);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull final SearchItemOrHeader oldItem, @NonNull final SearchItemOrHeader newItem) {
|
||||
return Objects.equals(oldItem, newItem);
|
||||
}
|
||||
};
|
||||
private static final String RECENT = "recent";
|
||||
private static final String FAVORITE = "favorite";
|
||||
private static final int VIEW_TYPE_HEADER = 0;
|
||||
private static final int VIEW_TYPE_ITEM = 1;
|
||||
|
||||
private final OnSearchItemClickListener onSearchItemClickListener;
|
||||
private final AsyncListDiffer<SearchItemOrHeader> differ;
|
||||
|
||||
public SearchItemsAdapter(final OnSearchItemClickListener onSearchItemClickListener) {
|
||||
differ = new AsyncListDiffer<>(new AdapterListUpdateCallback(this),
|
||||
new AsyncDifferConfig.Builder<>(DIFF_CALLBACK).build());
|
||||
this.onSearchItemClickListener = onSearchItemClickListener;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
|
||||
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
|
||||
if (viewType == VIEW_TYPE_HEADER) {
|
||||
return new HeaderViewHolder(ItemFavSectionHeaderBinding.inflate(layoutInflater, parent, false));
|
||||
}
|
||||
final ItemSearchResultBinding binding = ItemSearchResultBinding.inflate(layoutInflater, parent, false);
|
||||
return new SearchItemViewHolder(binding, onSearchItemClickListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position) {
|
||||
if (getItemViewType(position) == VIEW_TYPE_HEADER) {
|
||||
final SearchItemOrHeader searchItemOrHeader = getItem(position);
|
||||
if (!searchItemOrHeader.isHeader()) return;
|
||||
((HeaderViewHolder) holder).bind(searchItemOrHeader.header);
|
||||
return;
|
||||
}
|
||||
((SearchItemViewHolder) holder).bind(getItem(position).searchItem);
|
||||
}
|
||||
|
||||
protected SearchItemOrHeader getItem(int position) {
|
||||
return differ.getCurrentList().get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return differ.getCurrentList().size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(final int position) {
|
||||
return getItem(position).isHeader() ? VIEW_TYPE_HEADER : VIEW_TYPE_ITEM;
|
||||
}
|
||||
|
||||
public void submitList(@Nullable final List<SearchItem> list) {
|
||||
if (list == null) {
|
||||
differ.submitList(null);
|
||||
return;
|
||||
}
|
||||
differ.submitList(sectionAndSort(list));
|
||||
}
|
||||
|
||||
public void submitList(@Nullable final List<SearchItem> list, @Nullable final Runnable commitCallback) {
|
||||
if (list == null) {
|
||||
differ.submitList(null, commitCallback);
|
||||
return;
|
||||
}
|
||||
differ.submitList(sectionAndSort(list), commitCallback);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private List<SearchItemOrHeader> sectionAndSort(@NonNull final List<SearchItem> list) {
|
||||
final boolean containsRecentOrFavorite = list.stream().anyMatch(searchItem -> searchItem.isRecent() || searchItem.isFavorite());
|
||||
// Don't do anything if not showing recent results
|
||||
if (!containsRecentOrFavorite) {
|
||||
return list.stream()
|
||||
.map(SearchItemOrHeader::new)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
final List<SearchItem> listCopy = new ArrayList<>(list);
|
||||
Collections.sort(listCopy, (o1, o2) -> {
|
||||
final boolean bothRecent = o1.isRecent() && o2.isRecent();
|
||||
if (bothRecent) {
|
||||
// Don't sort
|
||||
return 0;
|
||||
}
|
||||
final boolean bothFavorite = o1.isFavorite() && o2.isFavorite();
|
||||
if (bothFavorite) {
|
||||
if (o1.getType() == o2.getType()) return 0;
|
||||
// keep users at top
|
||||
if (o1.getType() == FavoriteType.USER) return -1;
|
||||
if (o2.getType() == FavoriteType.USER) return 1;
|
||||
// keep locations at bottom
|
||||
if (o1.getType() == FavoriteType.LOCATION) return 1;
|
||||
if (o2.getType() == FavoriteType.LOCATION) return -1;
|
||||
}
|
||||
// keep recents at top
|
||||
if (o1.isRecent()) return -1;
|
||||
if (o2.isRecent()) return 1;
|
||||
return 0;
|
||||
});
|
||||
final List<SearchItemOrHeader> itemOrHeaders = new ArrayList<>();
|
||||
for (int i = 0; i < listCopy.size(); i++) {
|
||||
final SearchItem searchItem = listCopy.get(i);
|
||||
final SearchItemOrHeader prev = itemOrHeaders.isEmpty() ? null : itemOrHeaders.get(itemOrHeaders.size() - 1);
|
||||
boolean prevWasSameType = prev != null && ((prev.searchItem.isRecent() && searchItem.isRecent())
|
||||
|| (prev.searchItem.isFavorite() && searchItem.isFavorite()));
|
||||
if (prevWasSameType) {
|
||||
// just add the item
|
||||
itemOrHeaders.add(new SearchItemOrHeader(searchItem));
|
||||
continue;
|
||||
}
|
||||
// add header and item
|
||||
// add header only if search item is recent or favorite
|
||||
if (searchItem.isRecent() || searchItem.isFavorite()) {
|
||||
itemOrHeaders.add(new SearchItemOrHeader(searchItem.isRecent() ? RECENT : FAVORITE));
|
||||
}
|
||||
itemOrHeaders.add(new SearchItemOrHeader(searchItem));
|
||||
}
|
||||
return itemOrHeaders;
|
||||
}
|
||||
|
||||
private static class SearchItemOrHeader {
|
||||
String header;
|
||||
SearchItem searchItem;
|
||||
|
||||
public SearchItemOrHeader(final SearchItem searchItem) {
|
||||
this.searchItem = searchItem;
|
||||
}
|
||||
|
||||
public SearchItemOrHeader(final String header) {
|
||||
this.header = header;
|
||||
}
|
||||
|
||||
boolean isHeader() {
|
||||
return header != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
final SearchItemOrHeader that = (SearchItemOrHeader) o;
|
||||
return Objects.equals(header, that.header) &&
|
||||
Objects.equals(searchItem, that.searchItem);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(header, searchItem);
|
||||
}
|
||||
}
|
||||
|
||||
public static class HeaderViewHolder extends RecyclerView.ViewHolder {
|
||||
private final ItemFavSectionHeaderBinding binding;
|
||||
|
||||
public HeaderViewHolder(@NonNull final ItemFavSectionHeaderBinding binding) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
}
|
||||
|
||||
public void bind(final String header) {
|
||||
if (header == null) return;
|
||||
final int headerText;
|
||||
switch (header) {
|
||||
case RECENT:
|
||||
headerText = R.string.recent;
|
||||
break;
|
||||
case FAVORITE:
|
||||
headerText = R.string.title_favorites;
|
||||
break;
|
||||
default:
|
||||
headerText = R.string.unknown;
|
||||
break;
|
||||
}
|
||||
binding.getRoot().setText(headerText);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
package awais.instagrabber.adapters;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.cursoradapter.widget.CursorAdapter;
|
||||
|
||||
import awais.instagrabber.databinding.ItemSuggestionBinding;
|
||||
import awais.instagrabber.models.enums.SuggestionType;
|
||||
|
||||
public final class SuggestionsAdapter extends CursorAdapter {
|
||||
private static final String TAG = "SuggestionsAdapter";
|
||||
|
||||
private final OnSuggestionClickListener onSuggestionClickListener;
|
||||
|
||||
public SuggestionsAdapter(final Context context,
|
||||
final OnSuggestionClickListener onSuggestionClickListener) {
|
||||
super(context, null, FLAG_REGISTER_CONTENT_OBSERVER);
|
||||
this.onSuggestionClickListener = onSuggestionClickListener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View newView(final Context context, final Cursor cursor, final ViewGroup parent) {
|
||||
final LayoutInflater layoutInflater = LayoutInflater.from(context);
|
||||
final ItemSuggestionBinding binding = ItemSuggestionBinding.inflate(layoutInflater, parent, false);
|
||||
return binding.getRoot();
|
||||
// return layoutInflater.inflate(R.layout.item_suggestion, parent, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindView(@NonNull final View view, final Context context, @NonNull final Cursor cursor) {
|
||||
// i, username, fullname, type, query, picUrl, verified
|
||||
// 0, 1 , 2 , 3 , 4 , 5 , 6
|
||||
final String fullName = cursor.getString(2);
|
||||
String username = cursor.getString(1);
|
||||
String picUrl = cursor.getString(5);
|
||||
final boolean verified = cursor.getString(6).charAt(0) == 't';
|
||||
|
||||
final String type = cursor.getString(3);
|
||||
SuggestionType suggestionType = null;
|
||||
try {
|
||||
suggestionType = SuggestionType.valueOf(type);
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.e(TAG, "Unknown suggestion type: " + type, e);
|
||||
}
|
||||
if (suggestionType == null) return;
|
||||
String query = cursor.getString(4);
|
||||
switch (suggestionType) {
|
||||
case TYPE_USER:
|
||||
username = '@' + username;
|
||||
break;
|
||||
case TYPE_HASHTAG:
|
||||
username = '#' + username;
|
||||
break;
|
||||
}
|
||||
|
||||
if (onSuggestionClickListener != null) {
|
||||
final SuggestionType finalSuggestionType = suggestionType;
|
||||
view.setOnClickListener(v -> onSuggestionClickListener.onSuggestionClick(finalSuggestionType, query));
|
||||
}
|
||||
final ItemSuggestionBinding binding = ItemSuggestionBinding.bind(view);
|
||||
binding.isVerified.setVisibility(verified ? View.VISIBLE : View.GONE);
|
||||
binding.tvUsername.setText(username);
|
||||
binding.tvFullName.setVisibility(View.VISIBLE);
|
||||
binding.tvFullName.setText(fullName);
|
||||
binding.ivProfilePic.setImageURI(picUrl);
|
||||
}
|
||||
|
||||
public interface OnSuggestionClickListener {
|
||||
void onSuggestionClick(final SuggestionType type, final String query);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -55,7 +55,7 @@ public abstract class FeedItemViewHolder extends RecyclerView.ViewHolder {
|
||||
private void setupComments(@NonNull final Media feedModel) {
|
||||
final long commentsCount = feedModel.getCommentCount();
|
||||
bottomBinding.commentsCount.setText(String.valueOf(commentsCount));
|
||||
bottomBinding.commentsCount.setOnClickListener(v -> feedItemCallback.onCommentsClick(feedModel));
|
||||
bottomBinding.btnComments.setOnClickListener(v -> feedItemCallback.onCommentsClick(feedModel));
|
||||
}
|
||||
|
||||
private void setupProfilePic(@NonNull final Media media) {
|
||||
@ -75,6 +75,7 @@ public abstract class FeedItemViewHolder extends RecyclerView.ViewHolder {
|
||||
// final SpannableString spannableString = new SpannableString();
|
||||
// spannableString.setSpan(new CommentMentionClickSpan(), 0, titleLen, 0);
|
||||
final User user = media.getUser();
|
||||
if (user == null) return;
|
||||
final String title = "@" + user.getUsername();
|
||||
topBinding.title.setText(title);
|
||||
topBinding.title.setOnClickListener(v -> feedItemCallback.onNameClick(media, topBinding.ivProfilePic));
|
||||
@ -120,8 +121,7 @@ public abstract class FeedItemViewHolder extends RecyclerView.ViewHolder {
|
||||
topBinding.title.setLayoutParams(new RelativeLayout.LayoutParams(
|
||||
RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT
|
||||
));
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
final String locationName = location.getName();
|
||||
if (TextUtils.isEmpty(locationName)) {
|
||||
topBinding.location.setVisibility(View.GONE);
|
||||
|
@ -12,9 +12,6 @@ import awais.instagrabber.interfaces.FetchListener;
|
||||
import awais.instagrabber.repositories.responses.Media;
|
||||
import awais.instagrabber.utils.NetworkUtils;
|
||||
import awais.instagrabber.utils.ResponseBodyUtils;
|
||||
//import awaisomereport.LogCollector;
|
||||
|
||||
//import static awais.instagrabber.utils.Utils.logCollector;
|
||||
|
||||
public final class PostFetcher extends AsyncTask<Void, Void, Media> {
|
||||
private static final String TAG = "PostFetcher";
|
||||
@ -136,9 +133,9 @@ public final class PostFetcher extends AsyncTask<Void, Void, Media> {
|
||||
return ResponseBodyUtils.parseGraphQLItem(media, null);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// if (logCollector != null) {
|
||||
// logCollector.appendException(e, LogCollector.LogFile.ASYNC_POST_FETCHER, "doInBackground");
|
||||
// }
|
||||
// if (logCollector != null) {
|
||||
// logCollector.appendException(e, LogCollector.LogFile.ASYNC_POST_FETCHER, "doInBackground");
|
||||
// }
|
||||
Log.e(TAG, "Error fetching post", e);
|
||||
} finally {
|
||||
if (conn != null) {
|
||||
|
@ -57,6 +57,7 @@ public class RamboTextViewV2 extends AutoLinkTextView {
|
||||
}
|
||||
}
|
||||
});
|
||||
onAutoLinkLongClick(autoLinkItem -> {});
|
||||
}
|
||||
|
||||
public void addOnMentionClickListener(final OnMentionClickListener onMentionClickListener) {
|
||||
|
@ -14,8 +14,8 @@ import androidx.appcompat.widget.AppCompatTextView;
|
||||
import androidx.appcompat.widget.PopupMenu;
|
||||
|
||||
import com.facebook.drawee.backends.pipeline.Fresco;
|
||||
import com.facebook.drawee.backends.pipeline.PipelineDraweeControllerBuilder;
|
||||
import com.facebook.drawee.controller.BaseControllerListener;
|
||||
import com.facebook.drawee.interfaces.DraweeController;
|
||||
import com.facebook.imagepipeline.image.ImageInfo;
|
||||
import com.facebook.imagepipeline.request.ImageRequest;
|
||||
import com.facebook.imagepipeline.request.ImageRequestBuilder;
|
||||
@ -156,29 +156,33 @@ public class VideoPlayerViewHelper implements Player.EventListener {
|
||||
|
||||
private void setThumbnail() {
|
||||
binding.thumbnail.setAspectRatio(thumbnailAspectRatio);
|
||||
final ImageRequest thumbnailRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(thumbnailUrl))
|
||||
.build();
|
||||
final DraweeController controller = Fresco.newDraweeControllerBuilder()
|
||||
.setControllerListener(new BaseControllerListener<ImageInfo>() {
|
||||
@Override
|
||||
public void onFailure(final String id, final Throwable throwable) {
|
||||
if (videoPlayerCallback != null) {
|
||||
videoPlayerCallback.onThumbnailLoaded();
|
||||
}
|
||||
}
|
||||
ImageRequest thumbnailRequest = null;
|
||||
if (thumbnailUrl != null) {
|
||||
thumbnailRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(thumbnailUrl)).build();
|
||||
}
|
||||
final PipelineDraweeControllerBuilder builder = Fresco.newDraweeControllerBuilder()
|
||||
.setControllerListener(new BaseControllerListener<ImageInfo>() {
|
||||
@Override
|
||||
public void onFailure(final String id,
|
||||
final Throwable throwable) {
|
||||
if (videoPlayerCallback != null) {
|
||||
videoPlayerCallback.onThumbnailLoaded();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinalImageSet(final String id,
|
||||
final ImageInfo imageInfo,
|
||||
final Animatable animatable) {
|
||||
if (videoPlayerCallback != null) {
|
||||
videoPlayerCallback.onThumbnailLoaded();
|
||||
}
|
||||
}
|
||||
})
|
||||
.setImageRequest(thumbnailRequest)
|
||||
.build();
|
||||
binding.thumbnail.setController(controller);
|
||||
@Override
|
||||
public void onFinalImageSet(final String id,
|
||||
final ImageInfo imageInfo,
|
||||
final Animatable animatable) {
|
||||
if (videoPlayerCallback != null) {
|
||||
videoPlayerCallback.onThumbnailLoaded();
|
||||
}
|
||||
}
|
||||
});
|
||||
if (thumbnailRequest != null) {
|
||||
builder.setImageRequest(thumbnailRequest);
|
||||
}
|
||||
binding.thumbnail.setController(builder.build());
|
||||
}
|
||||
|
||||
private void loadPlayer() {
|
||||
|
@ -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
|
||||
|
@ -0,0 +1,37 @@
|
||||
package awais.instagrabber.db.dao;
|
||||
|
||||
import androidx.room.Dao;
|
||||
import androidx.room.Delete;
|
||||
import androidx.room.Insert;
|
||||
import androidx.room.Query;
|
||||
import androidx.room.Update;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import awais.instagrabber.db.entities.RecentSearch;
|
||||
import awais.instagrabber.models.enums.FavoriteType;
|
||||
|
||||
@Dao
|
||||
public interface RecentSearchDao {
|
||||
|
||||
@Query("SELECT * FROM recent_searches ORDER BY last_searched_on DESC")
|
||||
List<RecentSearch> getAllRecentSearches();
|
||||
|
||||
@Query("SELECT * FROM recent_searches WHERE `ig_id` = :igId AND `type` = :type")
|
||||
RecentSearch getRecentSearchByIgIdAndType(String igId, FavoriteType type);
|
||||
|
||||
@Query("SELECT * FROM recent_searches WHERE instr(`name`, :query) > 0")
|
||||
List<RecentSearch> findRecentSearchesWithNameContaining(String query);
|
||||
|
||||
@Insert
|
||||
Long insertRecentSearch(RecentSearch recentSearch);
|
||||
|
||||
@Update
|
||||
void updateRecentSearch(RecentSearch recentSearch);
|
||||
|
||||
@Delete
|
||||
void deleteRecentSearch(RecentSearch recentSearch);
|
||||
|
||||
// @Query("DELETE from recent_searches")
|
||||
// void deleteAllRecentSearches();
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
package awais.instagrabber.db.datasources;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import awais.instagrabber.db.AppDatabase;
|
||||
import awais.instagrabber.db.dao.RecentSearchDao;
|
||||
import awais.instagrabber.db.entities.RecentSearch;
|
||||
import awais.instagrabber.models.enums.FavoriteType;
|
||||
|
||||
public class RecentSearchDataSource {
|
||||
private static final String TAG = RecentSearchDataSource.class.getSimpleName();
|
||||
|
||||
private static RecentSearchDataSource INSTANCE;
|
||||
|
||||
private final RecentSearchDao recentSearchDao;
|
||||
|
||||
private RecentSearchDataSource(final RecentSearchDao recentSearchDao) {
|
||||
this.recentSearchDao = recentSearchDao;
|
||||
}
|
||||
|
||||
public static synchronized RecentSearchDataSource getInstance(@NonNull Context context) {
|
||||
if (INSTANCE == null) {
|
||||
synchronized (RecentSearchDataSource.class) {
|
||||
if (INSTANCE == null) {
|
||||
final AppDatabase database = AppDatabase.getDatabase(context);
|
||||
INSTANCE = new RecentSearchDataSource(database.recentSearchDao());
|
||||
}
|
||||
}
|
||||
}
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public RecentSearch getRecentSearchByIgIdAndType(@NonNull final String igId, @NonNull final FavoriteType type) {
|
||||
return recentSearchDao.getRecentSearchByIgIdAndType(igId, type);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public final List<RecentSearch> getAllRecentSearches() {
|
||||
return recentSearchDao.getAllRecentSearches();
|
||||
}
|
||||
|
||||
public final void insertOrUpdateRecentSearch(@NonNull final RecentSearch recentSearch) {
|
||||
if (recentSearch.getId() != 0) {
|
||||
recentSearchDao.updateRecentSearch(recentSearch);
|
||||
return;
|
||||
}
|
||||
recentSearchDao.insertRecentSearch(recentSearch);
|
||||
}
|
||||
|
||||
public final void deleteRecentSearch(@NonNull final RecentSearch recentSearch) {
|
||||
recentSearchDao.deleteRecentSearch(recentSearch);
|
||||
}
|
||||
}
|
@ -0,0 +1,185 @@
|
||||
package awais.instagrabber.db.entities;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.room.ColumnInfo;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.Ignore;
|
||||
import androidx.room.Index;
|
||||
import androidx.room.PrimaryKey;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Objects;
|
||||
|
||||
import awais.instagrabber.models.enums.FavoriteType;
|
||||
import awais.instagrabber.repositories.responses.search.SearchItem;
|
||||
|
||||
@Entity(tableName = RecentSearch.TABLE_NAME, indices = {@Index(value = {RecentSearch.COL_IG_ID, RecentSearch.COL_TYPE}, unique = true)})
|
||||
public class RecentSearch {
|
||||
private static final String TAG = RecentSearch.class.getSimpleName();
|
||||
|
||||
public static final String TABLE_NAME = "recent_searches";
|
||||
private static final String COL_ID = "id";
|
||||
public static final String COL_IG_ID = "ig_id";
|
||||
private static final String COL_NAME = "name";
|
||||
private static final String COL_USERNAME = "username";
|
||||
private static final String COL_PIC_URL = "pic_url";
|
||||
public static final String COL_TYPE = "type";
|
||||
private static final String COL_LAST_SEARCHED_ON = "last_searched_on";
|
||||
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@ColumnInfo(name = COL_ID)
|
||||
private final int id;
|
||||
|
||||
@ColumnInfo(name = COL_IG_ID)
|
||||
@NonNull
|
||||
private final String igId;
|
||||
|
||||
@ColumnInfo(name = COL_NAME)
|
||||
@NonNull
|
||||
private final String name;
|
||||
|
||||
@ColumnInfo(name = COL_USERNAME)
|
||||
private final String username;
|
||||
|
||||
@ColumnInfo(name = COL_PIC_URL)
|
||||
private final String picUrl;
|
||||
|
||||
@ColumnInfo(name = COL_TYPE)
|
||||
@NonNull
|
||||
private final FavoriteType type;
|
||||
|
||||
@ColumnInfo(name = COL_LAST_SEARCHED_ON)
|
||||
@NonNull
|
||||
private final LocalDateTime lastSearchedOn;
|
||||
|
||||
@Ignore
|
||||
public RecentSearch(final String igId,
|
||||
final String name,
|
||||
final String username,
|
||||
final String picUrl,
|
||||
final FavoriteType type,
|
||||
final LocalDateTime lastSearchedOn) {
|
||||
this(0, igId, name, username, picUrl, type, lastSearchedOn);
|
||||
}
|
||||
|
||||
public RecentSearch(final int id,
|
||||
@NonNull final String igId,
|
||||
@NonNull final String name,
|
||||
final String username,
|
||||
final String picUrl,
|
||||
@NonNull final FavoriteType type,
|
||||
@NonNull final LocalDateTime lastSearchedOn) {
|
||||
this.id = id;
|
||||
this.igId = igId;
|
||||
this.name = name;
|
||||
this.username = username;
|
||||
this.picUrl = picUrl;
|
||||
this.type = type;
|
||||
this.lastSearchedOn = lastSearchedOn;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getIgId() {
|
||||
return igId;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public String getPicUrl() {
|
||||
return picUrl;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public FavoriteType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public LocalDateTime getLastSearchedOn() {
|
||||
return lastSearchedOn;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
final RecentSearch that = (RecentSearch) o;
|
||||
return Objects.equals(igId, that.igId) &&
|
||||
Objects.equals(name, that.name) &&
|
||||
Objects.equals(username, that.username) &&
|
||||
Objects.equals(picUrl, that.picUrl) &&
|
||||
type == that.type &&
|
||||
Objects.equals(lastSearchedOn, that.lastSearchedOn);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(igId, name, username, picUrl, type, lastSearchedOn);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "RecentSearch{" +
|
||||
"id=" + id +
|
||||
", igId='" + igId + '\'' +
|
||||
", name='" + name + '\'' +
|
||||
", username='" + username + '\'' +
|
||||
", picUrl='" + picUrl + '\'' +
|
||||
", type=" + type +
|
||||
", lastSearchedOn=" + lastSearchedOn +
|
||||
'}';
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static RecentSearch fromSearchItem(@NonNull final SearchItem searchItem) {
|
||||
final FavoriteType type = searchItem.getType();
|
||||
if (type == null) return null;
|
||||
try {
|
||||
final String igId;
|
||||
final String name;
|
||||
final String username;
|
||||
final String picUrl;
|
||||
switch (type) {
|
||||
case USER:
|
||||
igId = String.valueOf(searchItem.getUser().getPk());
|
||||
name = searchItem.getUser().getFullName();
|
||||
username = searchItem.getUser().getUsername();
|
||||
picUrl = searchItem.getUser().getProfilePicUrl();
|
||||
break;
|
||||
case HASHTAG:
|
||||
igId = searchItem.getHashtag().getId();
|
||||
name = searchItem.getHashtag().getName();
|
||||
username = null;
|
||||
picUrl = null;
|
||||
break;
|
||||
case LOCATION:
|
||||
igId = String.valueOf(searchItem.getPlace().getLocation().getPk());
|
||||
name = searchItem.getPlace().getTitle();
|
||||
username = null;
|
||||
picUrl = null;
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
return new RecentSearch(igId, name, username, picUrl, type, LocalDateTime.now());
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "fromSearchItem: ", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,124 @@
|
||||
package awais.instagrabber.db.repositories;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import awais.instagrabber.db.datasources.RecentSearchDataSource;
|
||||
import awais.instagrabber.db.entities.RecentSearch;
|
||||
import awais.instagrabber.models.enums.FavoriteType;
|
||||
import awais.instagrabber.utils.AppExecutors;
|
||||
|
||||
public class RecentSearchRepository {
|
||||
private static final String TAG = RecentSearchRepository.class.getSimpleName();
|
||||
|
||||
private static RecentSearchRepository instance;
|
||||
|
||||
private final AppExecutors appExecutors;
|
||||
private final RecentSearchDataSource recentSearchDataSource;
|
||||
|
||||
private RecentSearchRepository(final AppExecutors appExecutors, final RecentSearchDataSource recentSearchDataSource) {
|
||||
this.appExecutors = appExecutors;
|
||||
this.recentSearchDataSource = recentSearchDataSource;
|
||||
}
|
||||
|
||||
public static RecentSearchRepository getInstance(final RecentSearchDataSource recentSearchDataSource) {
|
||||
if (instance == null) {
|
||||
instance = new RecentSearchRepository(AppExecutors.getInstance(), recentSearchDataSource);
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void getRecentSearch(@NonNull final String igId,
|
||||
@NonNull final FavoriteType type,
|
||||
final RepositoryCallback<RecentSearch> callback) {
|
||||
// request on the I/O thread
|
||||
appExecutors.diskIO().execute(() -> {
|
||||
final RecentSearch recentSearch = recentSearchDataSource.getRecentSearchByIgIdAndType(igId, type);
|
||||
// notify on the main thread
|
||||
appExecutors.mainThread().execute(() -> {
|
||||
if (callback == null) return;
|
||||
if (recentSearch == null) {
|
||||
callback.onDataNotAvailable();
|
||||
return;
|
||||
}
|
||||
callback.onSuccess(recentSearch);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public void getAllRecentSearches(final RepositoryCallback<List<RecentSearch>> callback) {
|
||||
// request on the I/O thread
|
||||
appExecutors.diskIO().execute(() -> {
|
||||
final List<RecentSearch> recentSearches = recentSearchDataSource.getAllRecentSearches();
|
||||
// notify on the main thread
|
||||
appExecutors.mainThread().execute(() -> {
|
||||
if (callback == null) return;
|
||||
callback.onSuccess(recentSearches);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public void insertOrUpdateRecentSearch(@NonNull final RecentSearch recentSearch,
|
||||
final RepositoryCallback<Void> callback) {
|
||||
insertOrUpdateRecentSearch(recentSearch.getIgId(), recentSearch.getName(), recentSearch.getUsername(), recentSearch.getPicUrl(),
|
||||
recentSearch.getType(), callback);
|
||||
}
|
||||
|
||||
public void insertOrUpdateRecentSearch(@NonNull final String igId,
|
||||
@NonNull final String name,
|
||||
final String username,
|
||||
final String picUrl,
|
||||
@NonNull final FavoriteType type,
|
||||
final RepositoryCallback<Void> callback) {
|
||||
// request on the I/O thread
|
||||
appExecutors.diskIO().execute(() -> {
|
||||
RecentSearch recentSearch = recentSearchDataSource.getRecentSearchByIgIdAndType(igId, type);
|
||||
recentSearch = recentSearch == null
|
||||
? new RecentSearch(igId, name, username, picUrl, type, LocalDateTime.now())
|
||||
: new RecentSearch(recentSearch.getId(), igId, name, username, picUrl, type, LocalDateTime.now());
|
||||
recentSearchDataSource.insertOrUpdateRecentSearch(recentSearch);
|
||||
// notify on the main thread
|
||||
appExecutors.mainThread().execute(() -> {
|
||||
if (callback == null) return;
|
||||
callback.onSuccess(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public void deleteRecentSearchByIgIdAndType(@NonNull final String igId,
|
||||
@NonNull final FavoriteType type,
|
||||
final RepositoryCallback<Void> callback) {
|
||||
// request on the I/O thread
|
||||
appExecutors.diskIO().execute(() -> {
|
||||
final RecentSearch recentSearch = recentSearchDataSource.getRecentSearchByIgIdAndType(igId, type);
|
||||
if (recentSearch != null) {
|
||||
recentSearchDataSource.deleteRecentSearch(recentSearch);
|
||||
}
|
||||
// notify on the main thread
|
||||
appExecutors.mainThread().execute(() -> {
|
||||
if (callback == null) return;
|
||||
if (recentSearch == null) {
|
||||
callback.onDataNotAvailable();
|
||||
return;
|
||||
}
|
||||
callback.onSuccess(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public void deleteRecentSearch(@NonNull final RecentSearch recentSearch,
|
||||
final RepositoryCallback<Void> callback) {
|
||||
// request on the I/O thread
|
||||
appExecutors.diskIO().execute(() -> {
|
||||
|
||||
recentSearchDataSource.deleteRecentSearch(recentSearch);
|
||||
// notify on the main thread
|
||||
appExecutors.mainThread().execute(() -> {
|
||||
if (callback == null) return;
|
||||
callback.onSuccess(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -25,6 +25,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.navigation.NavController;
|
||||
import androidx.navigation.NavDirections;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
@ -59,6 +60,7 @@ import awais.instagrabber.repositories.requests.StoryViewerOptions;
|
||||
import awais.instagrabber.repositories.responses.Hashtag;
|
||||
import awais.instagrabber.repositories.responses.Location;
|
||||
import awais.instagrabber.repositories.responses.Media;
|
||||
import awais.instagrabber.repositories.responses.User;
|
||||
import awais.instagrabber.utils.Constants;
|
||||
import awais.instagrabber.utils.CookieUtils;
|
||||
import awais.instagrabber.utils.DownloadUtils;
|
||||
@ -207,7 +209,9 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe
|
||||
final View mainPostImage,
|
||||
final int position) {
|
||||
if (opening) return;
|
||||
if (TextUtils.isEmpty(feedModel.getUser().getUsername())) {
|
||||
final User user = feedModel.getUser();
|
||||
if (user == null) return;
|
||||
if (TextUtils.isEmpty(user.getUsername())) {
|
||||
opening = true;
|
||||
new PostFetcher(feedModel.getCode(), newFeedModel -> {
|
||||
opening = false;
|
||||
@ -225,7 +229,9 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe
|
||||
builder.setSharedProfilePicElement(profilePicView)
|
||||
.setSharedMainPostElement(mainPostImage);
|
||||
}
|
||||
builder.build().show(getChildFragmentManager(), "post_view");
|
||||
final FragmentManager fragmentManager = getChildFragmentManager();
|
||||
if (fragmentManager.isDestroyed()) return;
|
||||
builder.build().show(fragmentManager, "post_view");
|
||||
opening = false;
|
||||
}
|
||||
};
|
||||
|
@ -23,6 +23,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.navigation.NavController;
|
||||
import androidx.navigation.NavDirections;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
@ -55,6 +56,7 @@ import awais.instagrabber.models.enums.FavoriteType;
|
||||
import awais.instagrabber.repositories.requests.StoryViewerOptions;
|
||||
import awais.instagrabber.repositories.responses.Location;
|
||||
import awais.instagrabber.repositories.responses.Media;
|
||||
import awais.instagrabber.repositories.responses.User;
|
||||
import awais.instagrabber.utils.Constants;
|
||||
import awais.instagrabber.utils.CookieUtils;
|
||||
import awais.instagrabber.utils.DownloadUtils;
|
||||
@ -198,7 +200,9 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR
|
||||
final View mainPostImage,
|
||||
final int position) {
|
||||
if (opening) return;
|
||||
if (TextUtils.isEmpty(feedModel.getUser().getUsername())) {
|
||||
final User user = feedModel.getUser();
|
||||
if (user == null) return;
|
||||
if (TextUtils.isEmpty(user.getUsername())) {
|
||||
opening = true;
|
||||
new PostFetcher(feedModel.getCode(), newFeedModel -> {
|
||||
opening = false;
|
||||
@ -217,7 +221,9 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR
|
||||
builder.setSharedProfilePicElement(profilePicView)
|
||||
.setSharedMainPostElement(mainPostImage);
|
||||
}
|
||||
builder.build().show(getChildFragmentManager(), "post_view");
|
||||
final FragmentManager fragmentManager = getChildFragmentManager();
|
||||
if (fragmentManager.isDestroyed()) return;
|
||||
builder.build().show(fragmentManager, "post_view");
|
||||
opening = false;
|
||||
}
|
||||
};
|
||||
@ -402,16 +408,16 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR
|
||||
final long locationId = locationModel.getPk();
|
||||
// binding.swipeRefreshLayout.setRefreshing(true);
|
||||
locationDetailsBinding.mainLocationImage.setImageURI("res:/" + R.drawable.ic_location);
|
||||
// final String postCount = String.valueOf(locationModel.getCount());
|
||||
// final SpannableStringBuilder span = new SpannableStringBuilder(getResources().getQuantityString(R.plurals.main_posts_count_inline,
|
||||
// locationModel.getPostCount() > 2000000000L
|
||||
// ? 2000000000
|
||||
// : locationModel.getPostCount().intValue(),
|
||||
// postCount));
|
||||
// span.setSpan(new RelativeSizeSpan(1.2f), 0, postCount.length(), 0);
|
||||
// span.setSpan(new StyleSpan(Typeface.BOLD), 0, postCount.length(), 0);
|
||||
// locationDetailsBinding.mainLocPostCount.setText(span);
|
||||
// locationDetailsBinding.mainLocPostCount.setVisibility(View.VISIBLE);
|
||||
// final String postCount = String.valueOf(locationModel.getCount());
|
||||
// final SpannableStringBuilder span = new SpannableStringBuilder(getResources().getQuantityString(R.plurals.main_posts_count_inline,
|
||||
// locationModel.getPostCount() > 2000000000L
|
||||
// ? 2000000000
|
||||
// : locationModel.getPostCount().intValue(),
|
||||
// postCount));
|
||||
// span.setSpan(new RelativeSizeSpan(1.2f), 0, postCount.length(), 0);
|
||||
// span.setSpan(new StyleSpan(Typeface.BOLD), 0, postCount.length(), 0);
|
||||
// locationDetailsBinding.mainLocPostCount.setText(span);
|
||||
// locationDetailsBinding.mainLocPostCount.setVisibility(View.VISIBLE);
|
||||
locationDetailsBinding.locationFullName.setText(locationModel.getName());
|
||||
CharSequence biography = locationModel.getAddress() + "\n" + locationModel.getCity();
|
||||
// binding.locationBiography.setCaptionIsExpandable(true);
|
||||
@ -424,22 +430,22 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR
|
||||
} else {
|
||||
locationDetailsBinding.locationBiography.setVisibility(View.VISIBLE);
|
||||
locationDetailsBinding.locationBiography.setText(biography);
|
||||
// locationDetailsBinding.locationBiography.addOnHashtagListener(autoLinkItem -> {
|
||||
// final NavController navController = NavHostFragment.findNavController(this);
|
||||
// final Bundle bundle = new Bundle();
|
||||
// final String originalText = autoLinkItem.getOriginalText().trim();
|
||||
// bundle.putString(ARG_HASHTAG, originalText);
|
||||
// navController.navigate(R.id.action_global_hashTagFragment, bundle);
|
||||
// });
|
||||
// locationDetailsBinding.locationBiography.addOnMentionClickListener(autoLinkItem -> {
|
||||
// final String originalText = autoLinkItem.getOriginalText().trim();
|
||||
// navigateToProfile(originalText);
|
||||
// });
|
||||
// locationDetailsBinding.locationBiography.addOnEmailClickListener(autoLinkItem -> Utils.openEmailAddress(context,
|
||||
// autoLinkItem.getOriginalText()
|
||||
// .trim()));
|
||||
// locationDetailsBinding.locationBiography
|
||||
// .addOnURLClickListener(autoLinkItem -> Utils.openURL(context, autoLinkItem.getOriginalText().trim()));
|
||||
// locationDetailsBinding.locationBiography.addOnHashtagListener(autoLinkItem -> {
|
||||
// final NavController navController = NavHostFragment.findNavController(this);
|
||||
// final Bundle bundle = new Bundle();
|
||||
// final String originalText = autoLinkItem.getOriginalText().trim();
|
||||
// bundle.putString(ARG_HASHTAG, originalText);
|
||||
// navController.navigate(R.id.action_global_hashTagFragment, bundle);
|
||||
// });
|
||||
// locationDetailsBinding.locationBiography.addOnMentionClickListener(autoLinkItem -> {
|
||||
// final String originalText = autoLinkItem.getOriginalText().trim();
|
||||
// navigateToProfile(originalText);
|
||||
// });
|
||||
// locationDetailsBinding.locationBiography.addOnEmailClickListener(autoLinkItem -> Utils.openEmailAddress(context,
|
||||
// autoLinkItem.getOriginalText()
|
||||
// .trim()));
|
||||
// locationDetailsBinding.locationBiography
|
||||
// .addOnURLClickListener(autoLinkItem -> Utils.openURL(context, autoLinkItem.getOriginalText().trim()));
|
||||
locationDetailsBinding.locationBiography.setOnLongClickListener(v -> {
|
||||
Utils.copyText(context, biography);
|
||||
return true;
|
||||
|
@ -331,6 +331,7 @@ public class PostViewV2Fragment extends SharedElementTransitionDialogFragment im
|
||||
if (bottomSheetBehavior != null) {
|
||||
captionState = bottomSheetBehavior.getState();
|
||||
}
|
||||
if (settingsHelper.getBoolean(Constants.PLAY_IN_BACKGROUND)) return;
|
||||
final Media media = viewModel.getMedia();
|
||||
if (media == null) return;
|
||||
switch (media.getMediaType()) {
|
||||
@ -609,7 +610,11 @@ public class PostViewV2Fragment extends SharedElementTransitionDialogFragment im
|
||||
bundle.putString("shortCode", media.getCode());
|
||||
bundle.putString("postId", media.getPk());
|
||||
bundle.putLong("postUserId", user.getPk());
|
||||
navController.navigate(R.id.action_global_commentsViewerFragment, bundle);
|
||||
try {
|
||||
navController.navigate(R.id.action_global_commentsViewerFragment, bundle);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "setupComment: ", e);
|
||||
}
|
||||
});
|
||||
binding.comment.setOnLongClickListener(v -> {
|
||||
Utils.displayToastAboveView(context, v, getString(R.string.comment));
|
||||
@ -994,7 +999,11 @@ public class PostViewV2Fragment extends SharedElementTransitionDialogFragment im
|
||||
addSharedElement(sharedMainPostElement, binding.postImage);
|
||||
}
|
||||
final Media media = viewModel.getMedia();
|
||||
final ImageRequest requestBuilder = ImageRequestBuilder.newBuilderWithSource(Uri.parse(ResponseBodyUtils.getImageUrl(media)))
|
||||
final String imageUrl = ResponseBodyUtils.getImageUrl(media);
|
||||
if (TextUtils.isEmpty(imageUrl)) {
|
||||
return;
|
||||
}
|
||||
final ImageRequest requestBuilder = ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageUrl))
|
||||
.setLocalThumbnailPreviewsEnabled(true)
|
||||
.build();
|
||||
final DraweeController controller = Fresco
|
||||
@ -1019,6 +1028,8 @@ public class PostViewV2Fragment extends SharedElementTransitionDialogFragment im
|
||||
// binding.postImage.setOnClickListener(v -> toggleDetails());
|
||||
final AnimatedZoomableController zoomableController = AnimatedZoomableController.newInstance();
|
||||
zoomableController.setMaxScaleFactor(3f);
|
||||
zoomableController.setGestureZoomEnabled(true);
|
||||
zoomableController.setEnabled(true);
|
||||
binding.postImage.setZoomableController(zoomableController);
|
||||
binding.postImage.setTapListener(new GestureDetector.SimpleOnGestureListener() {
|
||||
@Override
|
||||
|
@ -41,7 +41,7 @@ import awais.instagrabber.webservices.ServiceCallback;
|
||||
import static awais.instagrabber.utils.Utils.settingsHelper;
|
||||
|
||||
public class SavedCollectionsFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener {
|
||||
private static final String TAG = "SavedCollectionsFragment";
|
||||
private static final String TAG = SavedCollectionsFragment.class.getSimpleName();
|
||||
public static boolean pleaseRefresh = false;
|
||||
|
||||
private MainActivity fragmentActivity;
|
||||
@ -147,13 +147,16 @@ public class SavedCollectionsFragment extends Fragment implements SwipeRefreshLa
|
||||
if (isSaving) {
|
||||
setNavControllerResult(navController, topicCluster.getId());
|
||||
navController.navigateUp();
|
||||
}
|
||||
else {
|
||||
final FragmentNavigator.Extras.Builder builder = new FragmentNavigator.Extras.Builder()
|
||||
.addSharedElement(cover, "collection-" + topicCluster.getId());
|
||||
final SavedCollectionsFragmentDirections.ActionSavedCollectionsFragmentToCollectionPostsFragment action = SavedCollectionsFragmentDirections
|
||||
.actionSavedCollectionsFragmentToCollectionPostsFragment(topicCluster, titleColor, backgroundColor);
|
||||
navController.navigate(action, builder.build());
|
||||
} else {
|
||||
try {
|
||||
final FragmentNavigator.Extras.Builder builder = new FragmentNavigator.Extras.Builder()
|
||||
.addSharedElement(cover, "collection-" + topicCluster.getId());
|
||||
final SavedCollectionsFragmentDirections.ActionSavedCollectionsFragmentToCollectionPostsFragment action = SavedCollectionsFragmentDirections
|
||||
.actionSavedCollectionsFragmentToCollectionPostsFragment(topicCluster, titleColor, backgroundColor);
|
||||
navController.navigate(action, builder.build());
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "setupTopics: ", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
binding.topicsRecyclerView.setAdapter(adapter);
|
||||
|
@ -51,12 +51,15 @@ public final class StoryListViewerFragment extends Fragment implements SwipeRefr
|
||||
private AppCompatActivity fragmentActivity;
|
||||
private FragmentStoryListViewerBinding binding;
|
||||
private SwipeRefreshLayout root;
|
||||
private boolean shouldRefresh = true, firstRefresh = true;
|
||||
private boolean shouldRefresh = true;
|
||||
private boolean firstRefresh = true;
|
||||
private FeedStoriesViewModel feedStoriesViewModel;
|
||||
private ArchivesViewModel archivesViewModel;
|
||||
private StoriesService storiesService;
|
||||
private Context context;
|
||||
private String type, currentQuery, endCursor = null;
|
||||
private String type;
|
||||
private String currentQuery;
|
||||
private String endCursor = null;
|
||||
private FeedStoriesListAdapter adapter;
|
||||
private MenuItem menuSearch;
|
||||
|
||||
@ -226,7 +229,10 @@ public final class StoryListViewerFragment extends Fragment implements SwipeRefr
|
||||
binding.swipeRefreshLayout.setRefreshing(true);
|
||||
if (type.equals("feed") && firstRefresh) {
|
||||
binding.swipeRefreshLayout.setRefreshing(false);
|
||||
adapter.submitList(feedStoriesViewModel.getList().getValue());
|
||||
final List<FeedStoryModel> value = feedStoriesViewModel.getList().getValue();
|
||||
if (value != null) {
|
||||
adapter.submitList(value);
|
||||
}
|
||||
firstRefresh = false;
|
||||
} else if (type.equals("feed")) {
|
||||
storiesService.getFeedStories(new ServiceCallback<List<FeedStoryModel>>() {
|
||||
|
@ -413,10 +413,6 @@ public class StoryViewerFragment extends Fragment {
|
||||
return true;
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
// if (logCollector != null)
|
||||
// logCollector.appendException(e, LogCollector.LogFile.ACTIVITY_STORY_VIEWER, "setupListeners",
|
||||
// new Pair<>("swipeEvent", swipeEvent),
|
||||
// new Pair<>("diffX", diffX));
|
||||
if (BuildConfig.DEBUG) Log.e(TAG, "Error", e);
|
||||
}
|
||||
return false;
|
||||
@ -457,7 +453,7 @@ public class StoryViewerFragment extends Fragment {
|
||||
.setView(R.layout.dialog_opening_post)
|
||||
.create();
|
||||
alertDialog.show();
|
||||
mediaService.fetch(Long.valueOf(mediaId), new ServiceCallback<Media>() {
|
||||
mediaService.fetch(Long.parseLong(mediaId), new ServiceCallback<Media>() {
|
||||
@Override
|
||||
public void onSuccess(final Media feedModel) {
|
||||
final PostViewV2Fragment fragment = PostViewV2Fragment
|
||||
@ -834,7 +830,7 @@ public class StoryViewerFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshStory() {
|
||||
private synchronized void refreshStory() {
|
||||
if (binding.storiesList.getVisibility() == View.VISIBLE) {
|
||||
final List<StoryModel> storyModels = storiesViewModel.getList().getValue();
|
||||
if (storyModels != null && storyModels.size() > 0) {
|
||||
@ -1137,7 +1133,6 @@ public class StoryViewerFragment extends Fragment {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private void openProfile(final String username) {
|
||||
final ActionBar actionBar = fragmentActivity.getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
|
@ -139,7 +139,11 @@ public class DirectMessageInboxFragment extends Fragment implements SwipeRefresh
|
||||
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
|
||||
if (item.getItemId() == R.id.pending_requests) {
|
||||
final NavDirections directions = DirectMessageInboxFragmentDirections.actionInboxToPendingInbox();
|
||||
NavHostFragment.findNavController(this).navigate(directions);
|
||||
try {
|
||||
NavHostFragment.findNavController(this).navigate(directions);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "onOptionsItemSelected: ", e);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
@ -251,7 +255,11 @@ public class DirectMessageInboxFragment extends Fragment implements SwipeRefresh
|
||||
if (isAdded()) {
|
||||
final DirectMessageInboxFragmentDirections.ActionInboxToThread directions = DirectMessageInboxFragmentDirections
|
||||
.actionInboxToThread(thread.getThreadId(), thread.getThreadTitle());
|
||||
NavHostFragment.findNavController(this).navigate(directions);
|
||||
try {
|
||||
NavHostFragment.findNavController(this).navigate(directions);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "init: ", e);
|
||||
}
|
||||
}
|
||||
navigating = false;
|
||||
});
|
||||
|
@ -261,7 +261,9 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
|
||||
postViewV2Fragment.dismiss();
|
||||
binding.postsRecyclerView.refresh();
|
||||
});
|
||||
postViewV2Fragment.show(getChildFragmentManager(), "post_view");
|
||||
final FragmentManager fragmentManager = getChildFragmentManager();
|
||||
if (fragmentManager.isDestroyed() || fragmentManager.isStateSaved()) return;
|
||||
postViewV2Fragment.show(fragmentManager, "post_view");
|
||||
}
|
||||
};
|
||||
private final FeedAdapterV2.SelectionModeCallback selectionModeCallback = new FeedAdapterV2.SelectionModeCallback() {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -17,16 +17,26 @@ public class PostPreferencesFragment extends BasePreferencesFragment {
|
||||
final Context context = getContext();
|
||||
if (context == null) return;
|
||||
// generalCategory.addPreference(getAutoPlayVideosPreference(context));
|
||||
screen.addPreference(getBackgroundPlayPreference(context));
|
||||
screen.addPreference(getAlwaysMuteVideosPreference(context));
|
||||
screen.addPreference(getShowCaptionPreference(context));
|
||||
screen.addPreference(getToggleKeywordFilterPreference(context));
|
||||
screen.addPreference(getEditKeywordFilterPreference(context));
|
||||
}
|
||||
|
||||
private Preference getAutoPlayVideosPreference(@NonNull final Context context) {
|
||||
// private Preference getAutoPlayVideosPreference(@NonNull final Context context) {
|
||||
// final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context);
|
||||
// preference.setKey(Constants.AUTOPLAY_VIDEOS);
|
||||
// preference.setTitle(R.string.post_viewer_autoplay_video);
|
||||
// preference.setIconSpaceReserved(false);
|
||||
// return preference;
|
||||
// }
|
||||
|
||||
private Preference getBackgroundPlayPreference(@NonNull final Context context) {
|
||||
final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context);
|
||||
preference.setKey(Constants.AUTOPLAY_VIDEOS);
|
||||
preference.setTitle(R.string.post_viewer_autoplay_video);
|
||||
preference.setKey(Constants.PLAY_IN_BACKGROUND);
|
||||
preference.setTitle(R.string.post_viewer_background_play);
|
||||
preference.setSummary(R.string.post_viewer_background_play_summary);
|
||||
preference.setIconSpaceReserved(false);
|
||||
return preference;
|
||||
}
|
||||
|
@ -285,8 +285,13 @@ public final class InboxManager {
|
||||
if (index < 0) return;
|
||||
final List<DirectThread> threads = inbox.getThreads();
|
||||
final DirectThread thread = threads.get(index);
|
||||
thread.setItems(updatedItems);
|
||||
setThread(inbox, index, thread);
|
||||
try {
|
||||
final DirectThread threadClone = (DirectThread) thread.clone();
|
||||
threadClone.setItems(updatedItems);
|
||||
setThread(inbox, index, threadClone);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "setItemsToThread: ", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -92,14 +92,14 @@ public final class ThreadManager {
|
||||
private final MutableLiveData<DirectThreadParticipantRequestsResponse> pendingRequests = new MutableLiveData<>(null);
|
||||
|
||||
private final String threadId;
|
||||
private final DirectMessagesService service;
|
||||
private final long viewerId;
|
||||
private final ThreadIdOrUserIds threadIdOrUserIds;
|
||||
private final User currentUser;
|
||||
private final ContentResolver contentResolver;
|
||||
private final MediaService mediaService;
|
||||
private final FriendshipService friendshipService;
|
||||
|
||||
private DirectMessagesService service;
|
||||
private MediaService mediaService;
|
||||
private FriendshipService friendshipService;
|
||||
private InboxManager inboxManager;
|
||||
private LiveData<DirectThread> thread;
|
||||
private LiveData<Integer> inputMode;
|
||||
@ -157,6 +157,7 @@ public final class ThreadManager {
|
||||
viewerId = CookieUtils.getUserIdFromCookie(cookie);
|
||||
final String deviceUuid = settingsHelper.getString(Constants.DEVICE_UUID);
|
||||
final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie);
|
||||
if (csrfToken == null) return;
|
||||
// if (TextUtils.isEmpty(csrfToken) || viewerId <= 0 || TextUtils.isEmpty(deviceUuid)) {
|
||||
// throw new IllegalArgumentException("User is not logged in!");
|
||||
// }
|
||||
|
@ -1,7 +1,8 @@
|
||||
package awais.instagrabber.models.enums;
|
||||
|
||||
public enum FavoriteType {
|
||||
TOP, // used just for searching
|
||||
USER,
|
||||
HASHTAG,
|
||||
LOCATION
|
||||
LOCATION,
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -34,7 +34,7 @@ public class Media implements Serializable {
|
||||
private final boolean hasAudio;
|
||||
private final double videoDuration;
|
||||
private final long viewCount;
|
||||
private final Caption caption;
|
||||
private Caption caption;
|
||||
private final boolean canViewerSave;
|
||||
private final Audio audio;
|
||||
private final String title;
|
||||
@ -271,7 +271,14 @@ public class Media implements Serializable {
|
||||
}
|
||||
|
||||
public void setPostCaption(final String caption) {
|
||||
final Caption caption1 = getCaption();
|
||||
Caption caption1 = getCaption();
|
||||
if (caption1 == null) {
|
||||
final User user = getUser();
|
||||
if (user == null) return;
|
||||
caption1 = new Caption(user.getPk(), caption);
|
||||
this.caption = caption1;
|
||||
return;
|
||||
}
|
||||
caption1.setText(caption);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -229,7 +229,6 @@ public class DirectItem implements Cloneable, Serializable {
|
||||
public LocalDateTime getLocalDateTime() {
|
||||
if (localDateTime == null) {
|
||||
localDateTime = Instant.ofEpochMilli(timestamp / 1000).atZone(ZoneId.systemDefault()).toLocalDateTime();
|
||||
;
|
||||
}
|
||||
return localDateTime;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ public final class Constants {
|
||||
public static final String DOWNLOAD_USER_FOLDER = "download_user_folder";
|
||||
public static final String TOGGLE_KEYWORD_FILTER = "toggle_keyword_filter";
|
||||
public static final String DOWNLOAD_PREPEND_USER_NAME = "download_user_name";
|
||||
public static final String PLAY_IN_BACKGROUND = "play_in_background";
|
||||
// deprecated: public static final String BOTTOM_TOOLBAR = "bottom_toolbar";
|
||||
public static final String FOLDER_SAVE_TO = "saved_to";
|
||||
public static final String AUTOPLAY_VIDEOS = "autoplay_videos";
|
||||
|
@ -12,6 +12,9 @@ public final class IntentUtils {
|
||||
|
||||
@Nullable
|
||||
public static IntentModel parseUrl(@NonNull String url) {
|
||||
if (url.contains("instagr.am/")) {
|
||||
url = url.replaceFirst("s?://(?:www\\.)?instagr\\.am/", "s://www.instagram.com/");
|
||||
}
|
||||
final int wwwDel = url.contains("www.") ? 4 : 0;
|
||||
final boolean isHttps = url.startsWith("https");
|
||||
|
||||
|
@ -44,6 +44,7 @@ import static awais.instagrabber.utils.Constants.HIDE_MUTED_REELS;
|
||||
import static awais.instagrabber.utils.Constants.KEYWORD_FILTERS;
|
||||
import static awais.instagrabber.utils.Constants.MARK_AS_SEEN;
|
||||
import static awais.instagrabber.utils.Constants.MUTED_VIDEOS;
|
||||
import static awais.instagrabber.utils.Constants.PLAY_IN_BACKGROUND;
|
||||
import static awais.instagrabber.utils.Constants.PREF_DARK_THEME;
|
||||
import static awais.instagrabber.utils.Constants.PREF_EMOJI_VARIANTS;
|
||||
import static awais.instagrabber.utils.Constants.PREF_HASHTAG_POSTS_LAYOUT;
|
||||
@ -165,7 +166,7 @@ public final class SettingsHelper {
|
||||
@StringDef({DOWNLOAD_USER_FOLDER, DOWNLOAD_PREPEND_USER_NAME, FOLDER_SAVE_TO, AUTOPLAY_VIDEOS, SHOW_QUICK_ACCESS_DIALOG, MUTED_VIDEOS,
|
||||
SHOW_CAPTIONS, CUSTOM_DATE_TIME_FORMAT_ENABLED, MARK_AS_SEEN, DM_MARK_AS_SEEN, CHECK_ACTIVITY,
|
||||
CHECK_UPDATES, SWAP_DATE_TIME_FORMAT_ENABLED, PREF_ENABLE_DM_NOTIFICATIONS, PREF_ENABLE_DM_AUTO_REFRESH,
|
||||
FLAG_SECURE, TOGGLE_KEYWORD_FILTER, PREF_ENABLE_SENTRY, HIDE_MUTED_REELS})
|
||||
FLAG_SECURE, TOGGLE_KEYWORD_FILTER, PREF_ENABLE_SENTRY, HIDE_MUTED_REELS, PLAY_IN_BACKGROUND})
|
||||
public @interface BooleanSettings {}
|
||||
|
||||
@StringDef({PREV_INSTALL_VERSION, BROWSER_UA_CODE, APP_UA_CODE, PREF_ENABLE_DM_AUTO_REFRESH_FREQ_NUMBER})
|
||||
|
@ -94,13 +94,17 @@ public final class Utils {
|
||||
}
|
||||
|
||||
public static void copyText(@NonNull final Context context, final CharSequence string) {
|
||||
if (clipboardManager == null)
|
||||
if (clipboardManager == null) {
|
||||
clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
|
||||
}
|
||||
int toastMessage = R.string.clipboard_error;
|
||||
if (clipboardManager != null) {
|
||||
clipboardManager.setPrimaryClip(ClipData.newPlainText(context.getString(R.string.app_name), string));
|
||||
toastMessage = R.string.clipboard_copied;
|
||||
try {
|
||||
clipboardManager.setPrimaryClip(ClipData.newPlainText(context.getString(R.string.app_name), string));
|
||||
toastMessage = R.string.clipboard_copied;
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "copyText: ", e);
|
||||
}
|
||||
}
|
||||
Toast.makeText(context, toastMessage, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
@ -59,7 +60,7 @@ public class IgErrorsInterceptor implements Interceptor {
|
||||
return;
|
||||
case 302: // redirect
|
||||
final String location = response.header("location");
|
||||
if (location.equals("https://www.instagram.com/accounts/login/")) {
|
||||
if (location != null && location.equals("https://www.instagram.com/accounts/login/")) {
|
||||
// rate limited
|
||||
showErrorDialog(R.string.rate_limit);
|
||||
}
|
||||
@ -70,7 +71,7 @@ public class IgErrorsInterceptor implements Interceptor {
|
||||
try {
|
||||
final String bodyString = body.string();
|
||||
final JSONObject jsonObject = new JSONObject(bodyString);
|
||||
String message = jsonObject.optString("message", null);
|
||||
String message = jsonObject.optString("message");
|
||||
if (!TextUtils.isEmpty(message)) {
|
||||
message = message.toLowerCase();
|
||||
switch (message) {
|
||||
@ -91,7 +92,7 @@ public class IgErrorsInterceptor implements Interceptor {
|
||||
return;
|
||||
}
|
||||
}
|
||||
final String errorType = jsonObject.optString("error_type", null);
|
||||
final String errorType = jsonObject.optString("error_type");
|
||||
if (TextUtils.isEmpty(errorType)) return;
|
||||
if (errorType.equals("sentry_block")) {
|
||||
showErrorDialog(R.string.sentry_block);
|
||||
@ -127,7 +128,9 @@ public class IgErrorsInterceptor implements Interceptor {
|
||||
0,
|
||||
0
|
||||
);
|
||||
dialogFragment.show(mainActivity.getSupportFragmentManager(), "network_error_dialog");
|
||||
final FragmentManager fragmentManager = mainActivity.getSupportFragmentManager();
|
||||
if (fragmentManager.isStateSaved()) return;
|
||||
dialogFragment.show(fragmentManager, "network_error_dialog");
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 2.5 KiB |
@ -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>
|
||||
|
||||
|
@ -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" />
|
18
app/src/main/res/layout/fragment_search.xml
Normal 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>
|
41
app/src/main/res/layout/item_suggestion.xml → app/src/main/res/layout/item_search_result.xml
Executable file → Normal 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>
|
@ -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>
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_background.png
Normal file
After Width: | Height: | Size: 778 B |
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 6.5 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.4 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_background.png
Normal file
After Width: | Height: | Size: 540 B |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 3.4 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.4 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_background.png
Normal file
After Width: | Height: | Size: 1015 B |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 11 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 9.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 20 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 27 KiB |
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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">
|
||||
|
@ -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>
|
@ -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>
|
@ -56,6 +56,8 @@
|
||||
</plurals>
|
||||
<string name="main_posts_following">%s\n Seguint</string>
|
||||
<string name="post_viewer_autoplay_video">Reproduir vídeos automàticament</string>
|
||||
<string name="post_viewer_background_play">Continue videos in background</string>
|
||||
<string name="post_viewer_background_play_summary">Do not pause videos when the app is out of focus</string>
|
||||
<string name="post_viewer_muted_autoplay">Silenciar sempre els vídeos</string>
|
||||
<string name="post_viewer_show_captions">Mostrar sempre els subtítols</string>
|
||||
<string name="post_viewer_download_dialog_title">Seleccionar el que s\'ha de descarregar</string>
|
||||
|
@ -62,6 +62,8 @@
|
||||
</plurals>
|
||||
<string name="main_posts_following">%s\nSleduje</string>
|
||||
<string name="post_viewer_autoplay_video">Videa spouštět automaticky</string>
|
||||
<string name="post_viewer_background_play">Continue videos in background</string>
|
||||
<string name="post_viewer_background_play_summary">Do not pause videos when the app is out of focus</string>
|
||||
<string name="post_viewer_muted_autoplay">Vždy ztlumit videa</string>
|
||||
<string name="post_viewer_show_captions">Vždy zobrazovat titulek příspěvku</string>
|
||||
<string name="post_viewer_download_dialog_title">Vyberte, co stáhnout</string>
|
||||
|
@ -56,6 +56,8 @@
|
||||
</plurals>
|
||||
<string name="main_posts_following">%s\nAbonniert</string>
|
||||
<string name="post_viewer_autoplay_video">Videos automatisch abspielen</string>
|
||||
<string name="post_viewer_background_play">Continue videos in background</string>
|
||||
<string name="post_viewer_background_play_summary">Do not pause videos when the app is out of focus</string>
|
||||
<string name="post_viewer_muted_autoplay">Videos immer stummschalten</string>
|
||||
<string name="post_viewer_show_captions">Bildtext immer anzeigen</string>
|
||||
<string name="post_viewer_download_dialog_title">Datei zum Download auswählen</string>
|
||||
|
@ -39,8 +39,8 @@
|
||||
<string-array name="separator_presets">
|
||||
<item>Κανένα</item>
|
||||
<item>\@</item>
|
||||
<item>στο</item>
|
||||
<item>πάνω</item>
|
||||
<item>στις</item>
|
||||
<item>στις</item>
|
||||
<item>\|</item>
|
||||
<item>-</item>
|
||||
</string-array>
|
||||
|
@ -23,16 +23,16 @@
|
||||
<string name="update_check">Έλεγχος για ενημερώσεις κατά την εκκίνηση</string>
|
||||
<string name="flag_secure">Παρεμπόδιση στιγμιοτύπων οθόνης & προεπισκόπησης εφαρμογής</string>
|
||||
<string name="download_user_folder">Λήψη δημοσιεύσεων σε φακέλους ονομάτων χρηστών</string>
|
||||
<string name="download_prepend_username">Prepend Username to Filename</string>
|
||||
<string name="download_prepend_username">Προσθέστε το όνομα χρήστη πριν από το όνομα του αρχείου</string>
|
||||
<string name="mark_as_seen_setting">Επισήμανση ιστοριών ως προβληθέντων μετά την προβολή</string>
|
||||
<string name="mark_as_seen_setting_summary">Ο δημιουργός της ιστορίας θα γνωρίζει ότι προβλήθηκε</string>
|
||||
<string name="hide_muted_reels_setting">Hide muted stories from feed</string>
|
||||
<string name="hide_muted_reels_setting">Απόκρυψη ιστοριών που βρίσκονται σε σίγαση από τη ροή</string>
|
||||
<string name="dm_mark_as_seen_setting">Επισήμανση μηνυμάτων ως αναγνωσμένων μετά την προβολή</string>
|
||||
<string name="dm_mark_as_seen_setting_summary">Τα υπόλοιπα μέλη θα γνωρίζουν ότι προβλήθηκε</string>
|
||||
<string name="activity_setting">Ενεργοποίηση ειδοποιήσεων δραστηριότητας</string>
|
||||
<string name="story_sort_setting">Ταξινόμηση ροής ιστορίων</string>
|
||||
<string name="error_loading_profile">Σφάλμα κατά τη φόρτωση προφίλ! Είναι το όνομα χρήστη έγκυρο; Αν ναι, μπορεί να είστε περιορισμένος.</string>
|
||||
<string name="error_loading_profile_loggedin">Σφάλμα κατά τη φόρτωση λογαριασμού! Είναι το όνομα χρήστη έγκυρο; Μήπως σας έχει μπλοκάρει;</string>
|
||||
<string name="error_loading_profile_loggedin">Σφάλμα κατά τη φόρτωση λογαριασμού! Είναι το όνομα χρήστη έγκυρο; Μήπως ο χρήστης σας έχει μπλοκάρει;</string>
|
||||
<string name="error_loading_hashtag">Σφάλμα κατά τη φόρτωση hashtag! Είναι το όνομα έγκυρο;</string>
|
||||
<string name="error_loading_location">Σφάλμα κατά την φόρτωση τοποθεσίας! Είναι η διεύθυνση έγκυρη;</string>
|
||||
<string name="error_creating_folders">Σφάλμα κατά τη δημιουργία φακέλου/-ων λήψης.</string>
|
||||
@ -56,6 +56,8 @@
|
||||
</plurals>
|
||||
<string name="main_posts_following">%s\nΑκολουθείτε</string>
|
||||
<string name="post_viewer_autoplay_video">Αυτόματη αναπαραγωγή των βίντεο</string>
|
||||
<string name="post_viewer_background_play">Continue videos in background</string>
|
||||
<string name="post_viewer_background_play_summary">Do not pause videos when the app is out of focus</string>
|
||||
<string name="post_viewer_muted_autoplay">Μόνιμη σίγαση των βίντεο</string>
|
||||
<string name="post_viewer_show_captions">Μόνιμη εμφάνιση των λεζαντών των δημοσιεύσεων</string>
|
||||
<string name="post_viewer_download_dialog_title">Επιλογή δημοσιεύσεων για λήψη</string>
|
||||
@ -93,7 +95,7 @@
|
||||
<string name="logout">Αποσύνδεση</string>
|
||||
<string name="logout_summary">Ανώνυμη περιήγηση στο Instagram</string>
|
||||
<string name="remove_all_acc">Αφαίρεση όλων των λογαριασμών</string>
|
||||
<string name="remove_all_acc_warning">Αυτό θα αφαιρέσει όλους τους λογαριασμούς που έχουν προστεθεί στην εφαρμογή!\nΓια να αφαιρέσετε μόνο έναν λογαριασμό, πατήστε παρατεταμένα τον λογαριασμό από τον διάλογο εναλλαγής λογαριασμών.\nΘέλετε να συνεχίσετε;</string>
|
||||
<string name="remove_all_acc_warning">Έτσι, θα αφαιρεθούν όλοι οι λογαριασμοί που έχουν προστεθεί στην εφαρμογή!\nΓια να αφαιρέσετε μόνο έναν λογαριασμό, πατήστε τον παρατεταμένα από τον διάλογο εναλλαγής λογαριασμών.\nΘέλετε να συνεχίσετε;</string>
|
||||
<string name="time_settings">Μορφή ημερομηνίας</string>
|
||||
<string name="saved_create_collection">Δημιουργία νέας συλλογής</string>
|
||||
<string name="edit_collection">Επεξεργασία ονόματος συλλογής</string>
|
||||
@ -102,9 +104,9 @@
|
||||
<string name="delete_collection_note">Όλα τα πολυμέσα που περιέχονται θα παραμείνουν σε άλλες συλλογές.</string>
|
||||
<string name="add_to_collection">Προσθήκη στη συλλογή...</string>
|
||||
<string name="remove_from_collection">Αφαίρεση από τη συλλογή</string>
|
||||
<string name="liked">Επισημασμένο ως \"Μου αρέσει\"</string>
|
||||
<string name="liked">Μ\'αρέσουν</string>
|
||||
<string name="saved">Αποθηκευμένα</string>
|
||||
<string name="tagged">Σε αυτήν τη φωτογραφία</string>
|
||||
<string name="tagged">Ετικέτες</string>
|
||||
<string name="dm_person">Μήνυμα</string>
|
||||
<string name="like_without_count">Μου αρέσει</string>
|
||||
<string name="unlike_without_count">Δε μου αρέσει</string>
|
||||
@ -240,7 +242,7 @@
|
||||
<string name="action_notif">Δραστηριότητα</string>
|
||||
<string name="action_archive">Αρχειοθήκη ιστοριών</string>
|
||||
<string name="action_ayml">Προτεινόμενοι χρήστες</string>
|
||||
<string name="select_picture">Επιλέξτε Εικόνα</string>
|
||||
<string name="select_picture">Επιλογή εικόνας</string>
|
||||
<string name="uploading">Μεταφόρτωση…</string>
|
||||
<string name="activity_count_prefix">Έχετε:</string>
|
||||
<string name="activity_count_relationship">%d ακόλουθοι</string>
|
||||
@ -255,7 +257,7 @@
|
||||
<string name="profile">Προφίλ</string>
|
||||
<string name="more">Λοιπά</string>
|
||||
<string name="title_dm">Μηνύματα</string>
|
||||
<string name="number_selected">%d επιλέχθηκε</string>
|
||||
<string name="number_selected">Επιλέχθηκαν %d</string>
|
||||
<string name="logout_success">Η αποσύνδεση ήταν επιτυχής!</string>
|
||||
<string name="dm_thread_info">Πληροφορίες</string>
|
||||
<string name="mark_as_seen">Σήμανση ως αναγνωσμένο</string>
|
||||
@ -283,7 +285,7 @@
|
||||
<string name="light_theme_settings">Φωτεινό θέμα</string>
|
||||
<string name="dark_theme_settings">Σκούρο θέμα</string>
|
||||
<string name="light_barinsta_theme" comment="Yes, this one is Barista (the theme), you can also substitute it with other coffee-related words">Κόκκοι καφέ</string>
|
||||
<string name="dark_material_dark_theme">Ουσιώδες Σκούρο</string>
|
||||
<string name="dark_material_dark_theme">Σκουρόχρωμο</string>
|
||||
<string name="added_to_favs">Προστέθηκε στα Αγαπημένα!</string>
|
||||
<string name="add_to_favorites">Στα αγαπημένα</string>
|
||||
<string name="accounts">Λογαριασμοί</string>
|
||||
@ -292,7 +294,7 @@
|
||||
<string name="unknown">Άγνωστο</string>
|
||||
<string name="removed_from_favs">Αφαίρεση από τα αγαπημένα!</string>
|
||||
<string name="backup_and_restore">Αντίγραφα ασφαλείας & Επαναφορά</string>
|
||||
<string name="backup_summary">Δημιουργία αντιγράφου ασφαλείας των ρυθμίσεων της εφαρμογής, των δεδομένων σύνδεσης λογαριασμού και/ή αγαπημένα σε απλό κείμενο ή κρυπτογραφημένο αρχείο για μεταγενέστερη επαναφορά.</string>
|
||||
<string name="backup_summary">Δημιουργία αντιγράφου ασφαλείας των ρυθμίσεων της εφαρμογής, των δεδομένων σύνδεσης του λογαριασμού και/ή των αγαπημένων, σε ακρυπτογράφητο ή κρυπτογραφημένο αρχείο για μεταγενέστερη επαναφορά.</string>
|
||||
<string name="backup_warning">Αν δημιουργείτε αντίγραφα ασφαλείας των δεδομένων σύνδεσης λογαριασμού, αντιμετωπίστε το αρχείο ως απόρρητο και κρατήστε το σε ασφαλές μέρος!</string>
|
||||
<string name="create_backup">Δημιουργία νέου αρχείου αντιγράφου ασφαλείας</string>
|
||||
<string name="restore_backup">Επαναφορά από υπάρχον αρχείο αντιγράφου ασφαλείας</string>
|
||||
@ -327,8 +329,8 @@
|
||||
<string name="two">2</string>
|
||||
<string name="three">3</string>
|
||||
<string name="show_names">Εμφάνιση ονομάτων</string>
|
||||
<string name="show_avatars">Εμφάνιση άβαταρ</string>
|
||||
<string name="avatar_size">Μέγεθος άβαταρ</string>
|
||||
<string name="show_avatars">Εμφάνιση εικόνας προφίλ</string>
|
||||
<string name="avatar_size">Μέγεθος εικόνας προφίλ</string>
|
||||
<string name="corners">Γωνίες</string>
|
||||
<string name="show_grid_gap">Εμφάνιση κενού πλέγματος</string>
|
||||
<string name="disable_animation">Απενεργοποίηση κινουμένων σχεδίων</string>
|
||||
@ -461,22 +463,22 @@
|
||||
<string name="removed_keywords">Αφαιρέθηκε λέξη-κλειδί: %s στον κατάλογο φιλτραρίσματος</string>
|
||||
<string name="marked_as_seen">Επισήμανθηκε ως αναγνωσμένο</string>
|
||||
<string name="delete_unsuccessful">Η διαγραφή απέτυχε</string>
|
||||
<string name="throttle_error">Throttled by Instagram because of too many API requests. Wait for some time before retrying.</string>
|
||||
<string name="error">Error</string>
|
||||
<string name="account_logged_out">This account has been logged out.</string>
|
||||
<string name="login_required">Login required!</string>
|
||||
<string name="sentry_block">Sentry block.</string>
|
||||
<string name="inactive_user">User is inactive!</string>
|
||||
<string name="throttle_error">Περιορίστηκατε από το Instagram λόγω υπερβολικών αιτήσεων API. Περιμένετε ορισμένη ώρα προτού προσπαθήσετε ξανά.</string>
|
||||
<string name="error">Σφάλμα</string>
|
||||
<string name="account_logged_out">Αυτός ο λογαριασμός έχει αποσυνδεθεί.</string>
|
||||
<string name="login_required">Απαιτείται σύνδεση!</string>
|
||||
<string name="sentry_block">Φραγή Sentry.</string>
|
||||
<string name="inactive_user">Ο χρήστης είναι ανενεργός!</string>
|
||||
<string name="crash_report_subject">Αναφορά Κατάρρευσης Barinsta</string>
|
||||
<string name="crash_report_title">Επιλέξτε μια εφαρμογή ηλ. ταχυδρομείου για αποστολή αρχείων καταγραφής κατάρρευσης</string>
|
||||
<string name="not_found">Not found!</string>
|
||||
<string name="rate_limit">Your IP has been rate limited by Instagram. Wait for an hour and try again.</string>
|
||||
<string name="skip_update">Skip this update</string>
|
||||
<string name="on_latest_version">You\'re already on the latest version</string>
|
||||
<string name="tab_order">Screen order</string>
|
||||
<string name="other_tabs">Other tabs</string>
|
||||
<string name="tab_order_start_next_launch">The tab order will be reflected on next launch</string>
|
||||
<string name="dm_remove_warning">If saved, all DM related features will be disabled on next launch</string>
|
||||
<string name="not_found">Δε βρέθηκε!</string>
|
||||
<string name="rate_limit">Η διεύθυνση IP σας έχει περιοριστεί από το Instagram. Περιμένετε για μία ώρα και μετά προσπαθήστε ξανά.</string>
|
||||
<string name="skip_update">Παράλειψη της ενημέρωσης</string>
|
||||
<string name="on_latest_version">Η εφαρμογή είναι ήδη στην τελευταία έκδοση</string>
|
||||
<string name="tab_order">Σειρά της οθόνης</string>
|
||||
<string name="other_tabs">Λοιπές καρτέλες</string>
|
||||
<string name="tab_order_start_next_launch">Η σειρά των καρτελών θα ισχύσει από την επόμενη εκκίνηση</string>
|
||||
<string name="dm_remove_warning">Εάν αποθηκευτεί, όλες οι λειτουργίες που είναι σχετικές με τα Μηνύματα, θα είναι απενεργοποιημένες στην επόμενη εκκίνηση</string>
|
||||
<string name="copy_caption">Αντιγραφή λεζάντας</string>
|
||||
<string name="copy_reply">Αντιγραφή απάντησης</string>
|
||||
</resources>
|
||||
|
@ -56,6 +56,8 @@
|
||||
</plurals>
|
||||
<string name="main_posts_following">%s\nSiguiendo</string>
|
||||
<string name="post_viewer_autoplay_video">Autorreproducir vídeos</string>
|
||||
<string name="post_viewer_background_play">Continue videos in background</string>
|
||||
<string name="post_viewer_background_play_summary">Do not pause videos when the app is out of focus</string>
|
||||
<string name="post_viewer_muted_autoplay">Siempre silenciar vídeos</string>
|
||||
<string name="post_viewer_show_captions">Mostrar siempre subtítulos del post</string>
|
||||
<string name="post_viewer_download_dialog_title">Seleccionar qué descargar</string>
|
||||
@ -461,16 +463,16 @@
|
||||
<string name="removed_keywords">Se eliminó la palabra clave: %s de la lista de filtros</string>
|
||||
<string name="marked_as_seen">Marcado como visto</string>
|
||||
<string name="delete_unsuccessful">Eliminación fallida</string>
|
||||
<string name="throttle_error">Throttled by Instagram because of too many API requests. Wait for some time before retrying.</string>
|
||||
<string name="throttle_error">Restringido por Instagram por hacer demasiadas solicitudes de API. Espera un tiempo antes de reintentar.</string>
|
||||
<string name="error">Error</string>
|
||||
<string name="account_logged_out">This account has been logged out.</string>
|
||||
<string name="login_required">Login required!</string>
|
||||
<string name="sentry_block">Sentry block.</string>
|
||||
<string name="inactive_user">User is inactive!</string>
|
||||
<string name="account_logged_out">Esta cuenta ha sido desconectada.</string>
|
||||
<string name="login_required">¡Inicio de sesión requerido!</string>
|
||||
<string name="sentry_block">Bloqueo de Sentry.</string>
|
||||
<string name="inactive_user">¡Usuario inactivo!</string>
|
||||
<string name="crash_report_subject">Informe de fallos de Barinsta</string>
|
||||
<string name="crash_report_title">Seleccione una aplicación de correo electrónico para enviar registros de errores</string>
|
||||
<string name="not_found">Not found!</string>
|
||||
<string name="rate_limit">Your IP has been rate limited by Instagram. Wait for an hour and try again.</string>
|
||||
<string name="not_found">¡No encontrado!</string>
|
||||
<string name="rate_limit">Tu IP ha sido limitada por Instagram. Espera una hora e inténtalo de nuevo.</string>
|
||||
<string name="skip_update">Omitir esta actualización</string>
|
||||
<string name="on_latest_version">Ya tienes la última versión</string>
|
||||
<string name="tab_order">Orden de pantalla</string>
|
||||
|
@ -56,6 +56,8 @@
|
||||
</plurals>
|
||||
<string name="main_posts_following">%s\nJarraituak</string>
|
||||
<string name="post_viewer_autoplay_video">Erreproduzitu bideoak automatikoki</string>
|
||||
<string name="post_viewer_background_play">Continue videos in background</string>
|
||||
<string name="post_viewer_background_play_summary">Do not pause videos when the app is out of focus</string>
|
||||
<string name="post_viewer_muted_autoplay">Mututu bideoak beti</string>
|
||||
<string name="post_viewer_show_captions">Erakutsi argazki-oina beti</string>
|
||||
<string name="post_viewer_download_dialog_title">Hautatu zer deskargatu</string>
|
||||
|
@ -56,6 +56,8 @@
|
||||
</plurals>
|
||||
<string name="main_posts_following">%s\nدنبال کننده ها</string>
|
||||
<string name="post_viewer_autoplay_video">پخش خودکار فیلم ها</string>
|
||||
<string name="post_viewer_background_play">Continue videos in background</string>
|
||||
<string name="post_viewer_background_play_summary">Do not pause videos when the app is out of focus</string>
|
||||
<string name="post_viewer_muted_autoplay">همیشه فیلم هارو بی صدا کن</string>
|
||||
<string name="post_viewer_show_captions">Always show post captions</string>
|
||||
<string name="post_viewer_download_dialog_title">انتخاب کن چی دانلود کنی</string>
|
||||
|
@ -56,6 +56,8 @@
|
||||
</plurals>
|
||||
<string name="main_posts_following">%s\nAbonnements</string>
|
||||
<string name="post_viewer_autoplay_video">Lecture automatique des vidéos</string>
|
||||
<string name="post_viewer_background_play">Continue videos in background</string>
|
||||
<string name="post_viewer_background_play_summary">Do not pause videos when the app is out of focus</string>
|
||||
<string name="post_viewer_muted_autoplay">Toujours couper le son des vidéos</string>
|
||||
<string name="post_viewer_show_captions">Toujours afficher les sous-titres de publication</string>
|
||||
<string name="post_viewer_download_dialog_title">Sélectionnez ce que vous souhaitez télécharger</string>
|
||||
@ -461,7 +463,7 @@
|
||||
<string name="removed_keywords">Mot-clé supprimé : %s de la liste de filtres</string>
|
||||
<string name="marked_as_seen">Marqué comme vu</string>
|
||||
<string name="delete_unsuccessful">Suppression non réussie</string>
|
||||
<string name="throttle_error">Propulsé par Instagram à cause d\'un trop grand nombre de requêtes API. Attendez un certain temps avant de réessayer.</string>
|
||||
<string name="throttle_error">Limité par Instagram en raison d\'un trop grand nombre de requêtes API. Attendez un certain temps avant de réessayer.</string>
|
||||
<string name="error">Erreur</string>
|
||||
<string name="account_logged_out">Ce compte a été déconnecté.</string>
|
||||
<string name="login_required">Connexion requise!</string>
|
||||
|
@ -56,6 +56,8 @@
|
||||
</plurals>
|
||||
<string name="main_posts_following">%s\nFollowing</string>
|
||||
<string name="post_viewer_autoplay_video">वीडियो ऑटोप्ले करें</string>
|
||||
<string name="post_viewer_background_play">Continue videos in background</string>
|
||||
<string name="post_viewer_background_play_summary">Do not pause videos when the app is out of focus</string>
|
||||
<string name="post_viewer_muted_autoplay">सर्बदा वीडियो को शब्दहिन रखें</string>
|
||||
<string name="post_viewer_show_captions">Always show post captions</string>
|
||||
<string name="post_viewer_download_dialog_title">डाउनलोड करने के लिए चयन करें</string>
|
||||
|
@ -53,6 +53,8 @@
|
||||
</plurals>
|
||||
<string name="main_posts_following">%s\ndiikuti</string>
|
||||
<string name="post_viewer_autoplay_video">Otomatis putar video</string>
|
||||
<string name="post_viewer_background_play">Continue videos in background</string>
|
||||
<string name="post_viewer_background_play_summary">Do not pause videos when the app is out of focus</string>
|
||||
<string name="post_viewer_muted_autoplay">Selalu bisukan video</string>
|
||||
<string name="post_viewer_show_captions">Selalu tampilkan keterangan kiriman</string>
|
||||
<string name="post_viewer_download_dialog_title">Pilih apa yang akan diunduh</string>
|
||||
|
@ -56,6 +56,8 @@
|
||||
</plurals>
|
||||
<string name="main_posts_following">%s\nSeguiti</string>
|
||||
<string name="post_viewer_autoplay_video">Riproduzione automatica video</string>
|
||||
<string name="post_viewer_background_play">Continue videos in background</string>
|
||||
<string name="post_viewer_background_play_summary">Do not pause videos when the app is out of focus</string>
|
||||
<string name="post_viewer_muted_autoplay">Silenzia sempre i video</string>
|
||||
<string name="post_viewer_show_captions">Mostra sempre le didascalie dei post</string>
|
||||
<string name="post_viewer_download_dialog_title">Seleziona cosa scaricare</string>
|
||||
@ -461,16 +463,16 @@
|
||||
<string name="removed_keywords">Parola chiave rimossa: %s dalla lista filtri</string>
|
||||
<string name="marked_as_seen">Segnato come visto</string>
|
||||
<string name="delete_unsuccessful">Eliminazione non riuscita</string>
|
||||
<string name="throttle_error">Throttled by Instagram because of too many API requests. Wait for some time before retrying.</string>
|
||||
<string name="error">Error</string>
|
||||
<string name="account_logged_out">This account has been logged out.</string>
|
||||
<string name="login_required">Login required!</string>
|
||||
<string name="sentry_block">Sentry block.</string>
|
||||
<string name="inactive_user">User is inactive!</string>
|
||||
<string name="throttle_error">Limitato da Instagram a causa delle troppe richieste API. Aspetta un po\' prima di riprovare.</string>
|
||||
<string name="error">Errore</string>
|
||||
<string name="account_logged_out">Questo account è stato disconnesso.</string>
|
||||
<string name="login_required">Login richiesto!</string>
|
||||
<string name="sentry_block">Blocco sentinella.</string>
|
||||
<string name="inactive_user">Utente è inattivo!</string>
|
||||
<string name="crash_report_subject">Rapporto sugli errori di Barinsta</string>
|
||||
<string name="crash_report_title">Selezionare un\'applicazione di posta elettronica per inviare i registri di errori</string>
|
||||
<string name="not_found">Not found!</string>
|
||||
<string name="rate_limit">Your IP has been rate limited by Instagram. Wait for an hour and try again.</string>
|
||||
<string name="not_found">Non trovato!</string>
|
||||
<string name="rate_limit">Il tuo IP è stato limitato da Instagram. Aspetta un\'ora e riprova.</string>
|
||||
<string name="skip_update">Salta questo aggiornamento</string>
|
||||
<string name="on_latest_version">Hai già l\'ultima versione</string>
|
||||
<string name="tab_order">Ordine schermata</string>
|
||||
|
@ -53,6 +53,8 @@
|
||||
</plurals>
|
||||
<string name="main_posts_following">%s\nフォロー中</string>
|
||||
<string name="post_viewer_autoplay_video">動画を自動再生する</string>
|
||||
<string name="post_viewer_background_play">Continue videos in background</string>
|
||||
<string name="post_viewer_background_play_summary">Do not pause videos when the app is out of focus</string>
|
||||
<string name="post_viewer_muted_autoplay">動画を常にミュートする</string>
|
||||
<string name="post_viewer_show_captions">キャプションを常に表示</string>
|
||||
<string name="post_viewer_download_dialog_title">ダウンロード対象を選択</string>
|
||||
@ -299,7 +301,7 @@
|
||||
<string name="save">保存</string>
|
||||
<string name="caption">キャプション</string>
|
||||
<string name="edit_caption">Edit caption</string>
|
||||
<string name="translate_caption">Translate caption</string>
|
||||
<string name="translate_caption">翻訳を見る</string>
|
||||
<string name="player_timeline_desc">ビデオプレーヤーのタイムライン</string>
|
||||
<string name="liking">Liking…</string>
|
||||
<string name="like_unsuccessful">いいね!に失敗しました</string>
|
||||
|
@ -56,6 +56,8 @@
|
||||
</plurals>
|
||||
<string name="main_posts_following">%s\nСледбеници</string>
|
||||
<string name="post_viewer_autoplay_video">Autoplay на видеа</string>
|
||||
<string name="post_viewer_background_play">Continue videos in background</string>
|
||||
<string name="post_viewer_background_play_summary">Do not pause videos when the app is out of focus</string>
|
||||
<string name="post_viewer_muted_autoplay">Секогаш гледај видеа без звук</string>
|
||||
<string name="post_viewer_show_captions">Секогаш прикажувај наслов</string>
|
||||
<string name="post_viewer_download_dialog_title">Одбери што сакаш да превземеш</string>
|
||||
|