Browse Source
Add favorites, also fixes the hashtag follow, and some theming
renovate/org.robolectric-robolectric-4.x
Add favorites, also fixes the hashtag follow, and some theming
renovate/org.robolectric-robolectric-4.x
Ammar Githam
4 years ago
54 changed files with 1910 additions and 724 deletions
-
1.gitignore
-
2app/build.gradle
-
9app/src/main/java/awais/instagrabber/activities/MainActivity.java
-
202app/src/main/java/awais/instagrabber/adapters/FavoritesAdapter.java
-
61app/src/main/java/awais/instagrabber/adapters/viewholder/FavoriteViewHolder.java
-
7app/src/main/java/awais/instagrabber/asyncs/FeedFetcher.java
-
35app/src/main/java/awais/instagrabber/asyncs/HashtagFetcher.java
-
58app/src/main/java/awais/instagrabber/asyncs/PostsFetcher.java
-
3app/src/main/java/awais/instagrabber/asyncs/SuggestionsFetcher.java
-
70app/src/main/java/awais/instagrabber/asyncs/i/iLikedFetcher.java
-
181app/src/main/java/awais/instagrabber/dialogs/QuickAccessDialog.java
-
215app/src/main/java/awais/instagrabber/fragments/FavoritesFragment.java
-
161app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java
-
79app/src/main/java/awais/instagrabber/fragments/LocationFragment.java
-
21app/src/main/java/awais/instagrabber/fragments/SavedViewerFragment.java
-
306app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java
-
5app/src/main/java/awais/instagrabber/fragments/settings/MorePreferencesFragment.java
-
7app/src/main/java/awais/instagrabber/models/enums/FavoriteType.java
-
19app/src/main/java/awais/instagrabber/repositories/TagsRepository.java
-
1app/src/main/java/awais/instagrabber/utils/Constants.java
-
344app/src/main/java/awais/instagrabber/utils/DataBox.java
-
33app/src/main/java/awais/instagrabber/utils/ExportImportUtils.java
-
3app/src/main/java/awais/instagrabber/utils/NavigationExtensions.java
-
2app/src/main/java/awais/instagrabber/utils/Utils.java
-
19app/src/main/java/awais/instagrabber/viewmodels/FavoritesViewModel.java
-
101app/src/main/java/awais/instagrabber/webservices/TagsService.java
-
10app/src/main/res/drawable/ic_block_24.xml
-
10app/src/main/res/drawable/ic_highlight_off_24.xml
-
10app/src/main/res/drawable/ic_outline_class_24.xml
-
10app/src/main/res/drawable/ic_outline_map_24.xml
-
10app/src/main/res/drawable/ic_outline_person_add_24.xml
-
10app/src/main/res/drawable/ic_outline_person_add_disabled_24.xml
-
10app/src/main/res/drawable/ic_outline_person_pin_24.xml
-
10app/src/main/res/drawable/ic_outline_star_24.xml
-
10app/src/main/res/drawable/ic_outline_star_plus_24.xml
-
8app/src/main/res/drawable/ic_star_check_24.xml
-
2app/src/main/res/drawable/rounder_corner_semi_black_bg.xml
-
6app/src/main/res/drawable/sl_favourite_24.xml
-
1app/src/main/res/layout/activity_main.xml
-
2app/src/main/res/layout/dialog_profilepic.xml
-
7app/src/main/res/layout/fragment_favorites.xml
-
119app/src/main/res/layout/fragment_hashtag.xml
-
153app/src/main/res/layout/fragment_location.xml
-
122app/src/main/res/layout/fragment_profile.xml
-
13app/src/main/res/layout/item_fav_section_header.xml
-
2app/src/main/res/layout/item_feed_slider.xml
-
4app/src/main/res/layout/item_feed_top.xml
-
17app/src/main/res/layout/item_suggestion.xml
-
23app/src/main/res/menu/profile_menu.xml
-
25app/src/main/res/navigation/more_nav_graph.xml
-
64app/src/main/res/values/color.xml
-
10app/src/main/res/values/strings.xml
-
20app/src/main/res/values/styles.xml
-
1app/src/main/res/values/themes.xml
@ -0,0 +1,202 @@ |
|||
package awais.instagrabber.adapters; |
|||
|
|||
import android.view.LayoutInflater; |
|||
import android.view.ViewGroup; |
|||
|
|||
import androidx.annotation.NonNull; |
|||
import androidx.annotation.Nullable; |
|||
import androidx.core.util.ObjectsCompat; |
|||
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 awais.instagrabber.R; |
|||
import awais.instagrabber.adapters.viewholder.FavoriteViewHolder; |
|||
import awais.instagrabber.databinding.ItemFavSectionHeaderBinding; |
|||
import awais.instagrabber.databinding.ItemSuggestionBinding; |
|||
import awais.instagrabber.models.enums.FavoriteType; |
|||
import awais.instagrabber.utils.DataBox; |
|||
|
|||
public class FavoritesAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { |
|||
|
|||
private final OnFavoriteClickListener clickListener; |
|||
private final OnFavoriteLongClickListener longClickListener; |
|||
private final AsyncListDiffer<FavoriteModelOrHeader> differ; |
|||
|
|||
private static final DiffUtil.ItemCallback<FavoriteModelOrHeader> diffCallback = new DiffUtil.ItemCallback<FavoriteModelOrHeader>() { |
|||
@Override |
|||
public boolean areItemsTheSame(@NonNull final FavoriteModelOrHeader oldItem, @NonNull final FavoriteModelOrHeader newItem) { |
|||
boolean areSame = oldItem.isHeader() && newItem.isHeader(); |
|||
if (!areSame) { |
|||
return false; |
|||
} |
|||
if (oldItem.isHeader()) { |
|||
return ObjectsCompat.equals(oldItem.header, newItem.header); |
|||
} |
|||
if (oldItem.model != null && newItem.model != null) { |
|||
return oldItem.model.getId() == newItem.model.getId(); |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean areContentsTheSame(@NonNull final FavoriteModelOrHeader oldItem, @NonNull final FavoriteModelOrHeader newItem) { |
|||
boolean areSame = oldItem.isHeader() && newItem.isHeader(); |
|||
if (!areSame) { |
|||
return false; |
|||
} |
|||
if (oldItem.isHeader()) { |
|||
return ObjectsCompat.equals(oldItem.header, newItem.header); |
|||
} |
|||
return ObjectsCompat.equals(oldItem.model, newItem.model); |
|||
} |
|||
}; |
|||
|
|||
public FavoritesAdapter(final OnFavoriteClickListener clickListener, final OnFavoriteLongClickListener longClickListener) { |
|||
this.clickListener = clickListener; |
|||
this.longClickListener = longClickListener; |
|||
differ = new AsyncListDiffer<>(new AdapterListUpdateCallback(this), |
|||
new AsyncDifferConfig.Builder<>(diffCallback).build()); |
|||
} |
|||
|
|||
@NonNull |
|||
@Override |
|||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) { |
|||
final LayoutInflater inflater = LayoutInflater.from(parent.getContext()); |
|||
if (viewType == 0) { |
|||
// header |
|||
return new FavSectionViewHolder(ItemFavSectionHeaderBinding.inflate(inflater, parent, false)); |
|||
} |
|||
final ItemSuggestionBinding binding = ItemSuggestionBinding.inflate(inflater, parent, false); |
|||
return new FavoriteViewHolder(binding); |
|||
} |
|||
|
|||
@Override |
|||
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position) { |
|||
if (getItemViewType(position) == 0) { |
|||
final FavoriteModelOrHeader modelOrHeader = getItem(position); |
|||
if (!modelOrHeader.isHeader()) return; |
|||
((FavSectionViewHolder) holder).bind(modelOrHeader.header); |
|||
return; |
|||
} |
|||
((FavoriteViewHolder) holder).bind(getItem(position).model, clickListener, longClickListener); |
|||
} |
|||
|
|||
protected FavoriteModelOrHeader 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() ? 0 : 1; |
|||
} |
|||
|
|||
public void submitList(@Nullable final List<DataBox.FavoriteModel> list) { |
|||
if (list == null) { |
|||
differ.submitList(null); |
|||
return; |
|||
} |
|||
differ.submitList(sectionAndSort(list)); |
|||
} |
|||
|
|||
public void submitList(@Nullable final List<DataBox.FavoriteModel> list, @Nullable final Runnable commitCallback) { |
|||
if (list == null) { |
|||
differ.submitList(null, commitCallback); |
|||
return; |
|||
} |
|||
differ.submitList(sectionAndSort(list), commitCallback); |
|||
} |
|||
|
|||
@NonNull |
|||
private List<FavoriteModelOrHeader> sectionAndSort(@NonNull final List<DataBox.FavoriteModel> list) { |
|||
final List<DataBox.FavoriteModel> listCopy = new ArrayList<>(list); |
|||
Collections.sort(listCopy, (o1, o2) -> { |
|||
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; |
|||
return 0; |
|||
}); |
|||
final List<FavoriteModelOrHeader> modelOrHeaders = new ArrayList<>(); |
|||
for (int i = 0; i < listCopy.size(); i++) { |
|||
final DataBox.FavoriteModel model = listCopy.get(i); |
|||
final FavoriteModelOrHeader prev = modelOrHeaders.isEmpty() ? null : modelOrHeaders.get(modelOrHeaders.size() - 1); |
|||
boolean prevWasSameType = prev != null && prev.model.getType() == model.getType(); |
|||
if (prevWasSameType) { |
|||
// just add model |
|||
final FavoriteModelOrHeader modelOrHeader = new FavoriteModelOrHeader(); |
|||
modelOrHeader.model = model; |
|||
modelOrHeaders.add(modelOrHeader); |
|||
continue; |
|||
} |
|||
// add header and model |
|||
FavoriteModelOrHeader modelOrHeader = new FavoriteModelOrHeader(); |
|||
modelOrHeader.header = model.getType(); |
|||
modelOrHeaders.add(modelOrHeader); |
|||
modelOrHeader = new FavoriteModelOrHeader(); |
|||
modelOrHeader.model = model; |
|||
modelOrHeaders.add(modelOrHeader); |
|||
} |
|||
return modelOrHeaders; |
|||
} |
|||
|
|||
private static class FavoriteModelOrHeader { |
|||
FavoriteType header; |
|||
DataBox.FavoriteModel model; |
|||
|
|||
boolean isHeader() { |
|||
return header != null; |
|||
} |
|||
} |
|||
|
|||
public interface OnFavoriteClickListener { |
|||
void onClick(final DataBox.FavoriteModel model); |
|||
} |
|||
|
|||
public interface OnFavoriteLongClickListener { |
|||
boolean onLongClick(final DataBox.FavoriteModel model); |
|||
} |
|||
|
|||
public static class FavSectionViewHolder extends RecyclerView.ViewHolder { |
|||
private final ItemFavSectionHeaderBinding binding; |
|||
|
|||
public FavSectionViewHolder(@NonNull final ItemFavSectionHeaderBinding binding) { |
|||
super(binding.getRoot()); |
|||
this.binding = binding; |
|||
} |
|||
|
|||
public void bind(final FavoriteType header) { |
|||
if (header == null) return; |
|||
final int headerText; |
|||
switch (header) { |
|||
case USER: |
|||
headerText = R.string.accounts; |
|||
break; |
|||
case HASHTAG: |
|||
headerText = R.string.hashtags; |
|||
break; |
|||
case LOCATION: |
|||
headerText = R.string.locations; |
|||
break; |
|||
default: |
|||
headerText = R.string.unknown; |
|||
break; |
|||
} |
|||
binding.getRoot().setText(headerText); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,61 @@ |
|||
package awais.instagrabber.adapters.viewholder; |
|||
|
|||
import android.view.View; |
|||
|
|||
import androidx.annotation.NonNull; |
|||
import androidx.recyclerview.widget.RecyclerView; |
|||
|
|||
import awais.instagrabber.adapters.FavoritesAdapter; |
|||
import awais.instagrabber.databinding.ItemSuggestionBinding; |
|||
import awais.instagrabber.models.enums.FavoriteType; |
|||
import awais.instagrabber.utils.Constants; |
|||
import awais.instagrabber.utils.DataBox; |
|||
|
|||
public class FavoriteViewHolder extends RecyclerView.ViewHolder { |
|||
private static final String TAG = "FavoriteViewHolder"; |
|||
|
|||
private final ItemSuggestionBinding binding; |
|||
|
|||
public FavoriteViewHolder(@NonNull final ItemSuggestionBinding binding) { |
|||
super(binding.getRoot()); |
|||
this.binding = binding; |
|||
binding.isVerified.setVisibility(View.GONE); |
|||
} |
|||
|
|||
public void bind(final DataBox.FavoriteModel model, |
|||
final FavoritesAdapter.OnFavoriteClickListener clickListener, |
|||
final FavoritesAdapter.OnFavoriteLongClickListener longClickListener) { |
|||
// Log.d(TAG, "bind: " + model); |
|||
if (model == null) return; |
|||
itemView.setOnClickListener(v -> { |
|||
if (clickListener == null) return; |
|||
clickListener.onClick(model); |
|||
}); |
|||
itemView.setOnLongClickListener(v -> { |
|||
if (clickListener == null) return false; |
|||
return longClickListener.onLongClick(model); |
|||
}); |
|||
if (model.getType() == FavoriteType.HASHTAG) { |
|||
binding.ivProfilePic.setImageURI(Constants.DEFAULT_HASH_TAG_PIC); |
|||
} else { |
|||
binding.ivProfilePic.setImageURI(model.getPicUrl()); |
|||
} |
|||
binding.tvFullName.setText(model.getDisplayName()); |
|||
binding.tvUsername.setVisibility(View.VISIBLE); |
|||
String query = model.getQuery(); |
|||
switch (model.getType()) { |
|||
case HASHTAG: |
|||
query = "#" + query; |
|||
break; |
|||
case USER: |
|||
query = "@" + query; |
|||
break; |
|||
case LOCATION: |
|||
binding.tvUsername.setVisibility(View.GONE); |
|||
break; |
|||
default: |
|||
// do nothing |
|||
} |
|||
binding.tvUsername.setText(query); |
|||
} |
|||
} |
@ -1,181 +0,0 @@ |
|||
package awais.instagrabber.dialogs; |
|||
|
|||
import android.app.Activity; |
|||
import android.app.Dialog; |
|||
import android.content.Context; |
|||
import android.content.DialogInterface; |
|||
import android.content.pm.PackageManager; |
|||
import android.os.Bundle; |
|||
import android.view.View; |
|||
import android.widget.Toast; |
|||
|
|||
import androidx.annotation.NonNull; |
|||
import androidx.annotation.Nullable; |
|||
import androidx.appcompat.app.AlertDialog; |
|||
import androidx.core.content.ContextCompat; |
|||
import androidx.recyclerview.widget.DividerItemDecoration; |
|||
import androidx.recyclerview.widget.RecyclerView; |
|||
|
|||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment; |
|||
|
|||
import java.util.ArrayList; |
|||
|
|||
import awais.instagrabber.R; |
|||
import awais.instagrabber.adapters.SimpleAdapter; |
|||
import awais.instagrabber.utils.Constants; |
|||
import awais.instagrabber.utils.CookieUtils; |
|||
import awais.instagrabber.utils.DataBox; |
|||
import awais.instagrabber.utils.DownloadUtils; |
|||
import awais.instagrabber.utils.TextUtils; |
|||
import awais.instagrabber.utils.Utils; |
|||
|
|||
import static awais.instagrabber.utils.Utils.settingsHelper; |
|||
|
|||
public final class QuickAccessDialog extends BottomSheetDialogFragment implements DialogInterface.OnShowListener, |
|||
View.OnClickListener, View.OnLongClickListener { |
|||
private boolean cookieChanged, isQuery; |
|||
private Activity activity; |
|||
private String userQuery, displayName; |
|||
private View btnFavorite, btnImportExport; |
|||
private SimpleAdapter<DataBox.FavoriteModel> favoritesAdapter; |
|||
private RecyclerView rvFavorites, rvQuickAccess; |
|||
|
|||
public QuickAccessDialog setQuery(final String userQuery, final String displayName) { |
|||
this.userQuery = userQuery; |
|||
this.displayName = displayName; |
|||
return this; |
|||
} |
|||
|
|||
@NonNull |
|||
@Override |
|||
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { |
|||
final Dialog dialog = super.onCreateDialog(savedInstanceState); |
|||
|
|||
dialog.setOnShowListener(this); |
|||
|
|||
final Context context = getContext(); |
|||
activity = context instanceof Activity ? (Activity) context : getActivity(); |
|||
|
|||
final View contentView = View.inflate(activity, R.layout.dialog_quick_access, null); |
|||
|
|||
btnFavorite = contentView.findViewById(R.id.btnFavorite); |
|||
btnImportExport = contentView.findViewById(R.id.importExport); |
|||
|
|||
isQuery = !TextUtils.isEmpty(userQuery); |
|||
btnFavorite.setVisibility(isQuery ? View.VISIBLE : View.GONE); |
|||
Utils.setTooltipText(btnImportExport, R.string.import_export); |
|||
|
|||
favoritesAdapter = new SimpleAdapter<>(activity, Utils.dataBox.getAllFavorites(), this, this); |
|||
|
|||
btnFavorite.setOnClickListener(this); |
|||
btnImportExport.setOnClickListener(this); |
|||
|
|||
rvFavorites = contentView.findViewById(R.id.rvFavorites); |
|||
rvQuickAccess = contentView.findViewById(R.id.rvQuickAccess); |
|||
|
|||
final DividerItemDecoration itemDecoration = new DividerItemDecoration(activity, DividerItemDecoration.VERTICAL); |
|||
rvFavorites.addItemDecoration(itemDecoration); |
|||
rvFavorites.setAdapter(favoritesAdapter); |
|||
|
|||
final String cookieStr = settingsHelper.getString(Constants.COOKIE); |
|||
if (!TextUtils.isEmpty(cookieStr) |
|||
|| Utils.dataBox.getCookieCount() > 0 // fallback for export / import |
|||
) { |
|||
rvQuickAccess.addItemDecoration(itemDecoration); |
|||
final ArrayList<DataBox.CookieModel> allCookies = Utils.dataBox.getAllCookies(); |
|||
if (!TextUtils.isEmpty(cookieStr) && allCookies != null) { |
|||
for (final DataBox.CookieModel cookie : allCookies) { |
|||
if (cookieStr.equals(cookie.getCookie())) { |
|||
cookie.setSelected(true); |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
rvQuickAccess.setAdapter(new SimpleAdapter<>(activity, allCookies, this, this)); |
|||
} else { |
|||
((View) rvQuickAccess.getParent()).setVisibility(View.GONE); |
|||
} |
|||
|
|||
dialog.setContentView(contentView); |
|||
return dialog; |
|||
} |
|||
|
|||
@Override |
|||
public void onClick(@NonNull final View v) { |
|||
final Object tag = v.getTag(); |
|||
if (v == btnFavorite) { |
|||
if (isQuery) { |
|||
Utils.dataBox.addFavorite(new DataBox.FavoriteModel(userQuery, System.currentTimeMillis(), displayName)); |
|||
favoritesAdapter.setItems(Utils.dataBox.getAllFavorites()); |
|||
} |
|||
} else if (v == btnImportExport) { |
|||
if (ContextCompat.checkSelfPermission(activity, DownloadUtils.PERMS[0]) == PackageManager.PERMISSION_DENIED) |
|||
requestPermissions(DownloadUtils.PERMS, 6007); |
|||
else Utils.showImportExportDialog(v.getContext()); |
|||
|
|||
} else if (tag instanceof DataBox.FavoriteModel) { |
|||
// if (MainActivityBackup.scanHack != null) { |
|||
// MainActivityBackup.scanHack.onResult(((DataBox.FavoriteModel) tag).getQuery()); |
|||
// dismiss(); |
|||
// } |
|||
|
|||
} else if (tag instanceof DataBox.CookieModel) { |
|||
final DataBox.CookieModel cookieModel = (DataBox.CookieModel) tag; |
|||
if (!cookieModel.isSelected()) { |
|||
settingsHelper.putString(Constants.COOKIE, cookieModel.getCookie()); |
|||
CookieUtils.setupCookies(cookieModel.getCookie()); |
|||
cookieChanged = true; |
|||
} |
|||
dismiss(); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public boolean onLongClick(@NonNull final View v) { |
|||
final Object tag = v.getTag(); |
|||
|
|||
if (tag instanceof DataBox.FavoriteModel) { |
|||
final DataBox.FavoriteModel favoriteModel = (DataBox.FavoriteModel) tag; |
|||
|
|||
new AlertDialog.Builder(activity).setPositiveButton(R.string.yes, (d, which) -> { |
|||
Utils.dataBox.delFavorite(favoriteModel); |
|||
favoritesAdapter.setItems(Utils.dataBox.getAllFavorites()); |
|||
}) |
|||
.setNegativeButton(R.string.no, null).setMessage(getString(R.string.quick_access_confirm_delete, |
|||
favoriteModel.getQuery())).show(); |
|||
|
|||
} else if (tag instanceof DataBox.CookieModel) { |
|||
final DataBox.CookieModel cookieModel = (DataBox.CookieModel) tag; |
|||
|
|||
if (cookieModel.isSelected()) |
|||
Toast.makeText(v.getContext(), R.string.quick_access_cannot_delete_curr, Toast.LENGTH_SHORT).show(); |
|||
else |
|||
new AlertDialog.Builder(activity) |
|||
.setMessage(getString(R.string.quick_access_confirm_delete, cookieModel.getUsername())) |
|||
.setPositiveButton(R.string.yes, (d, which) -> { |
|||
Utils.dataBox.delUserCookie(cookieModel); |
|||
rvQuickAccess.findViewWithTag(cookieModel).setVisibility(View.GONE); |
|||
}) |
|||
.setNegativeButton(R.string.no, null) |
|||
.show(); |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
@Override |
|||
public void onDismiss(@NonNull final DialogInterface dialog) { |
|||
super.onDismiss(dialog); |
|||
if (cookieChanged && activity != null) activity.recreate(); |
|||
} |
|||
|
|||
@Override |
|||
public void onShow(final DialogInterface dialog) { |
|||
if (settingsHelper.getBoolean(Constants.SHOW_QUICK_ACCESS_DIALOG)) |
|||
new AlertDialog.Builder(activity) |
|||
.setMessage(R.string.quick_access_info_dialog) |
|||
.setPositiveButton(R.string.ok, null) |
|||
.setNeutralButton(R.string.dont_show_again, (d, which) -> |
|||
settingsHelper.putBoolean(Constants.SHOW_QUICK_ACCESS_DIALOG, false)).show(); |
|||
} |
|||
} |
@ -0,0 +1,215 @@ |
|||
package awais.instagrabber.fragments; |
|||
|
|||
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.lifecycle.ViewModelProvider; |
|||
import androidx.navigation.NavController; |
|||
import androidx.navigation.fragment.NavHostFragment; |
|||
import androidx.recyclerview.widget.LinearLayoutManager; |
|||
import androidx.recyclerview.widget.RecyclerView; |
|||
|
|||
import com.google.android.material.dialog.MaterialAlertDialogBuilder; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
import java.util.concurrent.BrokenBarrierException; |
|||
import java.util.concurrent.CyclicBarrier; |
|||
|
|||
import awais.instagrabber.R; |
|||
import awais.instagrabber.adapters.FavoritesAdapter; |
|||
import awais.instagrabber.asyncs.LocationFetcher; |
|||
import awais.instagrabber.asyncs.ProfileFetcher; |
|||
import awais.instagrabber.databinding.FragmentFavoritesBinding; |
|||
import awais.instagrabber.utils.DataBox; |
|||
import awais.instagrabber.utils.TextUtils; |
|||
import awais.instagrabber.utils.Utils; |
|||
import awais.instagrabber.viewmodels.FavoritesViewModel; |
|||
|
|||
public class FavoritesFragment extends Fragment { |
|||
private static final String TAG = "FavoritesFragment"; |
|||
|
|||
private boolean shouldRefresh = true; |
|||
private FragmentFavoritesBinding binding; |
|||
private RecyclerView root; |
|||
private FavoritesViewModel favoritesViewModel; |
|||
private FavoritesAdapter adapter; |
|||
|
|||
@NonNull |
|||
@Override |
|||
public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) { |
|||
if (root != null) { |
|||
shouldRefresh = false; |
|||
return root; |
|||
} |
|||
binding = FragmentFavoritesBinding.inflate(getLayoutInflater()); |
|||
root = binding.getRoot(); |
|||
binding.favoriteList.setLayoutManager(new LinearLayoutManager(getContext())); |
|||
return root; |
|||
} |
|||
|
|||
@Override |
|||
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { |
|||
if (!shouldRefresh) return; |
|||
init(); |
|||
shouldRefresh = false; |
|||
} |
|||
|
|||
@Override |
|||
public void onResume() { |
|||
super.onResume(); |
|||
if (favoritesViewModel == null || adapter == null) return; |
|||
// refresh list every time in onViewStateRestored since it is cheaper than implementing pull down to refresh |
|||
favoritesViewModel.getList().observe(getViewLifecycleOwner(), adapter::submitList); |
|||
final List<DataBox.FavoriteModel> allFavorites = Utils.dataBox.getAllFavorites(); |
|||
favoritesViewModel.getList().postValue(allFavorites); |
|||
fetchMissingInfo(allFavorites); |
|||
} |
|||
|
|||
private void init() { |
|||
favoritesViewModel = new ViewModelProvider(this).get(FavoritesViewModel.class); |
|||
adapter = new FavoritesAdapter(model -> { |
|||
// navigate |
|||
switch (model.getType()) { |
|||
case USER: { |
|||
final String username = model.getQuery(); |
|||
// Log.d(TAG, "username: " + username); |
|||
final NavController navController = NavHostFragment.findNavController(this); |
|||
final Bundle bundle = new Bundle(); |
|||
bundle.putString("username", "@" + username); |
|||
navController.navigate(R.id.action_global_profileFragment, bundle); |
|||
break; |
|||
} |
|||
case LOCATION: { |
|||
final String locationId = model.getQuery(); |
|||
// Log.d(TAG, "locationId: " + locationId); |
|||
final NavController navController = NavHostFragment.findNavController(this); |
|||
final Bundle bundle = new Bundle(); |
|||
bundle.putString("locationId", locationId); |
|||
navController.navigate(R.id.action_global_locationFragment, bundle); |
|||
break; |
|||
} |
|||
case HASHTAG: { |
|||
final String hashtag = model.getQuery(); |
|||
// Log.d(TAG, "hashtag: " + hashtag); |
|||
final NavController navController = NavHostFragment.findNavController(this); |
|||
final Bundle bundle = new Bundle(); |
|||
bundle.putString("hashtag", "#" + hashtag); |
|||
navController.navigate(R.id.action_global_hashTagFragment, bundle); |
|||
break; |
|||
} |
|||
default: |
|||
// do nothing |
|||
} |
|||
}, model -> { |
|||
// delete |
|||
final Context context = getContext(); |
|||
if (context == null) return false; |
|||
new MaterialAlertDialogBuilder(context) |
|||
.setMessage(getString(R.string.quick_access_confirm_delete, model.getQuery())) |
|||
.setPositiveButton(R.string.yes, (d, which) -> { |
|||
Utils.dataBox.deleteFavorite(model.getQuery(), model.getType()); |
|||
d.dismiss(); |
|||
favoritesViewModel.getList().postValue(Utils.dataBox.getAllFavorites()); |
|||
}) |
|||
.setNegativeButton(R.string.no, null) |
|||
.show(); |
|||
return true; |
|||
}); |
|||
binding.favoriteList.setAdapter(adapter); |
|||
// favoritesViewModel.getList().observe(getViewLifecycleOwner(), adapter::submitList); |
|||
|
|||
} |
|||
|
|||
private void fetchMissingInfo(final List<DataBox.FavoriteModel> allFavorites) { |
|||
final Runnable runnable = () -> { |
|||
final List<DataBox.FavoriteModel> updatedList = new ArrayList<>(allFavorites); |
|||
// cyclic barrier is to make the async calls synchronous |
|||
final CyclicBarrier cyclicBarrier = new CyclicBarrier(2, () -> { |
|||
// Log.d(TAG, "fetchMissingInfo: barrier action"); |
|||
favoritesViewModel.getList().postValue(new ArrayList<>(updatedList)); |
|||
}); |
|||
try { |
|||
for (final DataBox.FavoriteModel model : allFavorites) { |
|||
cyclicBarrier.reset(); |
|||
// if the model has missing pic or display name (for user and location), fetch those details |
|||
switch (model.getType()) { |
|||
case LOCATION: |
|||
if (TextUtils.isEmpty(model.getDisplayName()) |
|||
|| TextUtils.isEmpty(model.getPicUrl())) { |
|||
new LocationFetcher(model.getQuery(), result -> { |
|||
try { |
|||
if (result == null) return; |
|||
final int i = updatedList.indexOf(model); |
|||
updatedList.remove(i); |
|||
final DataBox.FavoriteModel updated = new DataBox.FavoriteModel( |
|||
model.getId(), |
|||
model.getQuery(), |
|||
model.getType(), |
|||
result.getName(), |
|||
result.getSdProfilePic(), |
|||
model.getDateAdded() |
|||
); |
|||
Utils.dataBox.addFavorite(updated); |
|||
updatedList.add(i, updated); |
|||
} finally { |
|||
try { |
|||
cyclicBarrier.await(); |
|||
} catch (BrokenBarrierException | InterruptedException e) { |
|||
Log.e(TAG, "fetchMissingInfo: ", e); |
|||
} |
|||
} |
|||
}).execute(); |
|||
cyclicBarrier.await(); |
|||
} |
|||
break; |
|||
case USER: |
|||
if (TextUtils.isEmpty(model.getDisplayName()) |
|||
|| TextUtils.isEmpty(model.getPicUrl())) { |
|||
new ProfileFetcher(model.getQuery(), result -> { |
|||
try { |
|||
if (result == null) return; |
|||
final int i = updatedList.indexOf(model); |
|||
updatedList.remove(i); |
|||
final DataBox.FavoriteModel updated = new DataBox.FavoriteModel( |
|||
model.getId(), |
|||
model.getQuery(), |
|||
model.getType(), |
|||
result.getName(), |
|||
result.getSdProfilePic(), |
|||
model.getDateAdded() |
|||
); |
|||
Utils.dataBox.addFavorite(updated); |
|||
updatedList.add(i, updated); |
|||
} finally { |
|||
try { |
|||
cyclicBarrier.await(); |
|||
} catch (BrokenBarrierException | InterruptedException e) { |
|||
Log.e(TAG, "fetchMissingInfo: ", e); |
|||
} |
|||
} |
|||
}).execute(); |
|||
cyclicBarrier.await(); |
|||
} |
|||
break; |
|||
case HASHTAG: |
|||
default: |
|||
// hashtags don't require displayName or pic |
|||
// updatedList.add(model); |
|||
} |
|||
} |
|||
} catch (Exception e) { |
|||
Log.e(TAG, "fetchMissingInfo: ", e); |
|||
} |
|||
favoritesViewModel.getList().postValue(updatedList); |
|||
}; |
|||
new Thread(runnable).start(); |
|||
} |
|||
} |
@ -0,0 +1,7 @@ |
|||
package awais.instagrabber.models.enums; |
|||
|
|||
public enum FavoriteType { |
|||
USER, |
|||
HASHTAG, |
|||
LOCATION |
|||
} |
@ -0,0 +1,19 @@ |
|||
package awais.instagrabber.repositories; |
|||
|
|||
import retrofit2.Call; |
|||
import retrofit2.http.Header; |
|||
import retrofit2.http.POST; |
|||
import retrofit2.http.Path; |
|||
|
|||
public interface TagsRepository { |
|||
|
|||
@POST("/web/tags/follow/{tag}/") |
|||
Call<String> follow(@Header("User-Agent") String userAgent, |
|||
@Header("x-csrftoken") String csrfToken, |
|||
@Path("tag") String tag); |
|||
|
|||
@POST("/web/tags/unfollow/{tag}/") |
|||
Call<String> unfollow(@Header("User-Agent") String userAgent, |
|||
@Header("x-csrftoken") String csrfToken, |
|||
@Path("tag") String tag); |
|||
} |
@ -0,0 +1,19 @@ |
|||
package awais.instagrabber.viewmodels; |
|||
|
|||
import androidx.lifecycle.MutableLiveData; |
|||
import androidx.lifecycle.ViewModel; |
|||
|
|||
import java.util.List; |
|||
|
|||
import awais.instagrabber.utils.DataBox; |
|||
|
|||
public class FavoritesViewModel extends ViewModel { |
|||
private MutableLiveData<List<DataBox.FavoriteModel>> list; |
|||
|
|||
public MutableLiveData<List<DataBox.FavoriteModel>> getList() { |
|||
if (list == null) { |
|||
list = new MutableLiveData<>(); |
|||
} |
|||
return list; |
|||
} |
|||
} |
@ -0,0 +1,101 @@ |
|||
package awais.instagrabber.webservices; |
|||
|
|||
import android.util.Log; |
|||
|
|||
import androidx.annotation.NonNull; |
|||
|
|||
import org.json.JSONException; |
|||
import org.json.JSONObject; |
|||
|
|||
import awais.instagrabber.repositories.TagsRepository; |
|||
import awais.instagrabber.utils.Constants; |
|||
import retrofit2.Call; |
|||
import retrofit2.Callback; |
|||
import retrofit2.Response; |
|||
import retrofit2.Retrofit; |
|||
|
|||
public class TagsService extends BaseService { |
|||
|
|||
private static final String TAG = "TagsService"; |
|||
|
|||
// web for www.instagram.com |
|||
private final TagsRepository webRepository; |
|||
|
|||
private static TagsService instance; |
|||
|
|||
private TagsService() { |
|||
final Retrofit webRetrofit = getRetrofitBuilder() |
|||
.baseUrl("https://www.instagram.com/") |
|||
.build(); |
|||
webRepository = webRetrofit.create(TagsRepository.class); |
|||
} |
|||
|
|||
public static TagsService getInstance() { |
|||
if (instance == null) { |
|||
instance = new TagsService(); |
|||
} |
|||
return instance; |
|||
} |
|||
|
|||
public void follow(@NonNull final String tag, |
|||
@NonNull final String csrfToken, |
|||
final ServiceCallback<Boolean> callback) { |
|||
final Call<String> request = webRepository.follow(Constants.USER_AGENT, |
|||
csrfToken, |
|||
tag); |
|||
request.enqueue(new Callback<String>() { |
|||
@Override |
|||
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) { |
|||
final String body = response.body(); |
|||
if (body == null) { |
|||
callback.onFailure(new RuntimeException("body is null")); |
|||
return; |
|||
} |
|||
try { |
|||
final JSONObject jsonObject = new JSONObject(body); |
|||
final String status = jsonObject.optString("status"); |
|||
callback.onSuccess(status.equals("ok")); |
|||
} catch (JSONException e) { |
|||
Log.e(TAG, "onResponse: ", e); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) { |
|||
// Log.e(TAG, "onFailure: ", t); |
|||
callback.onFailure(t); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
public void unfollow(@NonNull final String tag, |
|||
@NonNull final String csrfToken, |
|||
final ServiceCallback<Boolean> callback) { |
|||
final Call<String> request = webRepository.unfollow(Constants.USER_AGENT, |
|||
csrfToken, |
|||
tag); |
|||
request.enqueue(new Callback<String>() { |
|||
@Override |
|||
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) { |
|||
final String body = response.body(); |
|||
if (body == null) { |
|||
callback.onFailure(new RuntimeException("body is null")); |
|||
return; |
|||
} |
|||
try { |
|||
final JSONObject jsonObject = new JSONObject(body); |
|||
final String status = jsonObject.optString("status"); |
|||
callback.onSuccess(status.equals("ok")); |
|||
} catch (JSONException e) { |
|||
Log.e(TAG, "onResponse: ", e); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) { |
|||
// Log.e(TAG, "onFailure: ", t); |
|||
callback.onFailure(t); |
|||
} |
|||
}); |
|||
} |
|||
} |
@ -0,0 +1,10 @@ |
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
|||
android:width="24dp" |
|||
android:height="24dp" |
|||
android:viewportWidth="24" |
|||
android:viewportHeight="24" |
|||
android:tint="?attr/colorControlNormal"> |
|||
<path |
|||
android:fillColor="@android:color/white" |
|||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM4,12c0,-4.42 3.58,-8 8,-8 1.85,0 3.55,0.63 4.9,1.69L5.69,16.9C4.63,15.55 4,13.85 4,12zM12,20c-1.85,0 -3.55,-0.63 -4.9,-1.69L18.31,7.1C19.37,8.45 20,10.15 20,12c0,4.42 -3.58,8 -8,8z"/> |
|||
</vector> |
@ -0,0 +1,10 @@ |
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
|||
android:width="24dp" |
|||
android:height="24dp" |
|||
android:viewportWidth="24" |
|||
android:viewportHeight="24" |
|||
android:tint="?attr/colorControlNormal"> |
|||
<path |
|||
android:fillColor="@android:color/white" |
|||
android:pathData="M14.59,8L12,10.59 9.41,8 8,9.41 10.59,12 8,14.59 9.41,16 12,13.41 14.59,16 16,14.59 13.41,12 16,9.41 14.59,8zM12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z"/> |
|||
</vector> |
@ -0,0 +1,10 @@ |
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
|||
android:width="24dp" |
|||
android:height="24dp" |
|||
android:viewportWidth="24" |
|||
android:viewportHeight="24" |
|||
android:tint="?attr/colorControlNormal"> |
|||
<path |
|||
android:fillColor="@android:color/white" |
|||
android:pathData="M18,2L6,2c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,4c0,-1.1 -0.9,-2 -2,-2zM9,4h2v5l-1,-0.75L9,9L9,4zM18,20L6,20L6,4h1v9l3,-2.25L13,13L13,4h5v16z"/> |
|||
</vector> |
@ -0,0 +1,10 @@ |
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
|||
android:width="24dp" |
|||
android:height="24dp" |
|||
android:viewportWidth="24" |
|||
android:viewportHeight="24" |
|||
android:tint="?attr/colorControlNormal"> |
|||
<path |
|||
android:fillColor="@android:color/white" |
|||
android:pathData="M20.5,3l-0.16,0.03L15,5.1 9,3 3.36,4.9c-0.21,0.07 -0.36,0.25 -0.36,0.48L3,20.5c0,0.28 0.22,0.5 0.5,0.5l0.16,-0.03L9,18.9l6,2.1 5.64,-1.9c0.21,-0.07 0.36,-0.25 0.36,-0.48L21,3.5c0,-0.28 -0.22,-0.5 -0.5,-0.5zM10,5.47l4,1.4v11.66l-4,-1.4L10,5.47zM5,6.46l3,-1.01v11.7l-3,1.16L5,6.46zM19,17.54l-3,1.01L16,6.86l3,-1.16v11.84z"/> |
|||
</vector> |
@ -0,0 +1,10 @@ |
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
|||
android:width="24dp" |
|||
android:height="24dp" |
|||
android:viewportWidth="24" |
|||
android:viewportHeight="24" |
|||
android:tint="?attr/colorControlNormal"> |
|||
<path |
|||
android:fillColor="@android:color/white" |
|||
android:pathData="M15,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM15,6c1.1,0 2,0.9 2,2s-0.9,2 -2,2 -2,-0.9 -2,-2 0.9,-2 2,-2zM15,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4zM9,18c0.22,-0.72 3.31,-2 6,-2 2.7,0 5.8,1.29 6,2L9,18zM6,15v-3h3v-2L6,10L6,7L4,7v3L1,10v2h3v3z"/> |
|||
</vector> |
@ -0,0 +1,10 @@ |
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
|||
android:width="24dp" |
|||
android:height="24dp" |
|||
android:viewportWidth="24" |
|||
android:viewportHeight="24" |
|||
android:tint="?attr/colorControlNormal"> |
|||
<path |
|||
android:fillColor="@android:color/white" |
|||
android:pathData="M15,6c1.1,0 2,0.9 2,2 0,0.99 -0.73,1.82 -1.67,1.97l-2.31,-2.31C13.19,6.72 14.01,6 15,6m0,-2c-2.21,0 -4,1.79 -4,4 0,0.18 0.03,0.35 0.05,0.52l3.43,3.43c0.17,0.02 0.34,0.05 0.52,0.05 2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4zM16.69,14.16L22.53,20L23,20v-2c0,-2.14 -3.56,-3.5 -6.31,-3.84zM13.01,16.13L14.88,18L9,18c0.08,-0.24 0.88,-1.01 2.91,-1.57l1.1,-0.3M1.41,1.71L0,3.12l4,4L4,10L1,10v2h3v3h2v-3h2.88l2.51,2.51C9.19,15.11 7,16.3 7,18v2h9.88l4,4 1.41,-1.41L1.41,1.71zM6,10v-0.88l0.88,0.88L6,10z"/> |
|||
</vector> |
@ -0,0 +1,10 @@ |
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
|||
android:width="24dp" |
|||
android:height="24dp" |
|||
android:viewportWidth="24" |
|||
android:viewportHeight="24" |
|||
android:tint="?attr/colorControlNormal"> |
|||
<path |
|||
android:fillColor="@android:color/white" |
|||
android:pathData="M19,2L5,2c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h4l3,3 3,-3h4c1.1,0 2,-0.9 2,-2L21,4c0,-1.1 -0.9,-2 -2,-2zM19,18h-4.83l-0.59,0.59L12,20.17l-1.59,-1.59 -0.58,-0.58L5,18L5,4h14v14zM12,11c1.65,0 3,-1.35 3,-3s-1.35,-3 -3,-3 -3,1.35 -3,3 1.35,3 3,3zM12,7c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM18,15.58c0,-2.5 -3.97,-3.58 -6,-3.58s-6,1.08 -6,3.58L6,17h12v-1.42zM8.48,15c0.74,-0.51 2.23,-1 3.52,-1s2.78,0.49 3.52,1L8.48,15z"/> |
|||
</vector> |
@ -0,0 +1,10 @@ |
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
|||
android:width="24dp" |
|||
android:height="24dp" |
|||
android:viewportWidth="24" |
|||
android:viewportHeight="24" |
|||
android:tint="?attr/colorControlNormal"> |
|||
<path |
|||
android:fillColor="@android:color/white" |
|||
android:pathData="M22,9.24l-7.19,-0.62L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27 18.18,21l-1.63,-7.03L22,9.24zM12,15.4l-3.76,2.27 1,-4.28 -3.32,-2.88 4.38,-0.38L12,6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z"/> |
|||
</vector> |
@ -0,0 +1,10 @@ |
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
|||
android:width="24dp" |
|||
android:height="24dp" |
|||
android:tint="?attr/colorControlNormal" |
|||
android:viewportWidth="24" |
|||
android:viewportHeight="24"> |
|||
<path |
|||
android:fillColor="#000" |
|||
android:pathData="M5.8 21L7.4 14L2 9.2L9.2 8.6L12 2L14.8 8.6L22 9.2L18.8 12H18C17.3 12 16.6 12.1 15.9 12.4L18.1 10.5L13.7 10.1L12 6.1L10.3 10.1L5.9 10.5L9.2 13.4L8.2 17.7L12 15.4L12.5 15.7C12.3 16.2 12.1 16.8 12.1 17.3L5.8 21M17 14V17H14V19H17V22H19V19H22V17H19V14H17Z" /> |
|||
</vector> |
@ -0,0 +1,8 @@ |
|||
<!-- drawable/star_check.xml --> |
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
|||
android:height="24dp" |
|||
android:width="24dp" |
|||
android:viewportWidth="24" |
|||
android:viewportHeight="24"> |
|||
<path android:fillColor="#000" android:pathData="M5.8 21L7.4 14L2 9.2L9.2 8.6L12 2L14.8 8.6L22 9.2L18.8 12H18C14.9 12 12.4 14.3 12 17.3L5.8 21M17.8 21.2L22.6 16.4L21.3 15L17.7 18.6L16.2 17L15 18.2L17.8 21.2" /> |
|||
</vector> |
@ -0,0 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<selector xmlns:android="http://schemas.android.com/apk/res/android"> |
|||
<item android:drawable="@drawable/ic_outline_star_plus_24" android:state_checked="false" /> |
|||
<item android:drawable="@drawable/ic_star_check_24" android:state_checked="true" /> |
|||
<item android:drawable="@drawable/ic_outline_star_plus_24" /> |
|||
</selector> |
@ -0,0 +1,7 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android" |
|||
xmlns:tools="http://schemas.android.com/tools" |
|||
android:id="@+id/favorite_list" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="match_parent" |
|||
tools:listitem="@layout/item_suggestion" /> |
@ -0,0 +1,13 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<androidx.appcompat.widget.AppCompatTextView xmlns:android="http://schemas.android.com/apk/res/android" |
|||
xmlns:tools="http://schemas.android.com/tools" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" |
|||
android:paddingLeft="16dp" |
|||
android:paddingTop="16dp" |
|||
android:paddingRight="16dp" |
|||
android:paddingBottom="8dp" |
|||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2" |
|||
android:textColor="?colorAccent" |
|||
android:textStyle="bold" |
|||
tools:text="HEADERS" /> |
@ -1,12 +1,25 @@ |
|||
<?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"--> |
|||
<!-- android:visible="false"--> |
|||
<!-- app:showAsAction="ifRoom" />--> |
|||
|
|||
<item |
|||
android:id="@+id/favourites" |
|||
android:enabled="true" |
|||
android:icon="@drawable/ic_star_24" |
|||
android:title="@string/title_favorites" |
|||
android:id="@+id/block" |
|||
android:icon="@drawable/ic_block_24" |
|||
android:title="@string/block" |
|||
android:visible="false" |
|||
app:showAsAction="ifRoom" /> |
|||
app:showAsAction="never" /> |
|||
|
|||
<item |
|||
android:id="@+id/restrict" |
|||
android:icon="@drawable/ic_highlight_off_24" |
|||
android:title="@string/restrict" |
|||
android:visible="false" |
|||
app:showAsAction="never" /> |
|||
</menu> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue