package me.devsaki.hentoid.adapters;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.text.TextUtils;
import android.util.SparseBooleanArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import com.bumptech.glide.Glide;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import me.devsaki.hentoid.R;
import me.devsaki.hentoid.database.HentoidDB;
import me.devsaki.hentoid.database.domains.Attribute;
import me.devsaki.hentoid.database.domains.Content;
import me.devsaki.hentoid.database.domains.ImageFile;
import me.devsaki.hentoid.enums.AttributeType;
import me.devsaki.hentoid.enums.StatusContent;
import me.devsaki.hentoid.listener.ItemClickListener;
import me.devsaki.hentoid.listener.ItemClickListener.ItemSelectListener;
import me.devsaki.hentoid.services.DownloadService;
import me.devsaki.hentoid.util.FileHelper;
import me.devsaki.hentoid.util.Helper;
import me.devsaki.hentoid.util.LogHelper;
/**
* Created by avluis on 04/23/2016.
* RecyclerView based Content Adapter
*/
public class ContentAdapter extends RecyclerView.Adapter<ViewHolder> {
private static final String TAG = LogHelper.makeLogTag(ContentAdapter.class);
private static final int VISIBLE_THRESHOLD = 10;
private static final int VIEW_TYPE_LOADING = 0;
private static final int VIEW_TYPE_ITEM = 1;
private final Context cxt;
private final SparseBooleanArray selectedItems;
private final ItemSelectListener listener;
private boolean isFooterEnabled = true;
private ContentsWipedListener contentsWipedListener;
private EndlessScrollListener endlessScrollListener;
private List<Content> contents = new ArrayList<>();
public ContentAdapter(Context cxt, final List<Content> contents, ItemSelectListener listener) {
this.cxt = cxt;
this.contents = contents;
this.listener = listener;
selectedItems = new SparseBooleanArray();
}
public void setEndlessScrollListener(EndlessScrollListener listener) {
this.endlessScrollListener = listener;
}
public void setContentsWipedListener(ContentsWipedListener listener) {
this.contentsWipedListener = listener;
}
private void toggleSelection(int pos) {
if (selectedItems.get(pos, false)) {
selectedItems.delete(pos);
LogHelper.d(TAG, "Removed item: " + pos);
} else {
selectedItems.put(pos, true);
LogHelper.d(TAG, "Added item: " + pos);
}
notifyItemChanged(pos);
}
public void clearSelections() {
selectedItems.clear();
notifyDataSetChanged();
}
private int getSelectedItemCount() {
return selectedItems.size();
}
private List<Integer> getSelectedItems() {
List<Integer> items = new ArrayList<>(selectedItems.size());
for (int i = 0; i < selectedItems.size(); i++) {
items.add(selectedItems.keyAt(i));
}
return items;
}
private boolean getSelectedItem(int item) {
for (int i = 0; i < selectedItems.size(); i++) {
if (selectedItems.keyAt(i) == item) {
return selectedItems.get(item);
}
}
return false;
}
public void setContentList(List<Content> contentList) {
this.contents = contentList;
updateContentList();
}
public void updateContentList() {
this.notifyDataSetChanged();
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ViewHolder viewHolder;
if (viewType == VIEW_TYPE_LOADING) {
View view = LayoutInflater.from(parent.getContext()).inflate(
R.layout.progress, parent, false);
viewHolder = new ProgressViewHolder(view);
} else {
View view = LayoutInflater.from(parent.getContext()).inflate(
R.layout.item_download, parent, false);
viewHolder = new ContentHolder(view);
}
return viewHolder;
}
@Override
public void onBindViewHolder(final ViewHolder holder, final int pos) {
if (holder instanceof ProgressViewHolder) {
((ProgressViewHolder) holder).progressBar.setIndeterminate(true);
} else if (contents.size() > 0 && pos < contents.size()) {
final Content content = contents.get(pos);
updateLayoutVisibility((ContentHolder) holder, content, pos);
populateLayout((ContentHolder) holder, content, pos);
attachOnClickListeners((ContentHolder) holder, content, pos);
}
}
private void updateLayoutVisibility(ContentHolder holder, Content content, int pos) {
if (pos == getItemCount() - VISIBLE_THRESHOLD && endlessScrollListener != null) {
endlessScrollListener.onLoadMore();
}
if (endlessScrollListener == null) {
enableFooter(false);
}
if (getSelectedItems() != null) {
int itemPos = holder.getLayoutPosition();
boolean selected = getSelectedItem(itemPos);
if (getSelectedItem(itemPos)) {
holder.itemView.setSelected(selected);
} else {
holder.itemView.setSelected(false);
}
}
final RelativeLayout items = (RelativeLayout) holder.itemView.findViewById(R.id.item);
LinearLayout minimal = (LinearLayout) holder.itemView.findViewById(R.id.item_minimal);
if (holder.itemView.isSelected()) {
LogHelper.d(TAG, "Position: " + pos + ": " + content.getTitle()
+ " is a selected item currently in view.");
if (getSelectedItemCount() >= 1) {
items.setVisibility(View.GONE);
minimal.setVisibility(View.VISIBLE);
}
} else {
items.setVisibility(View.VISIBLE);
minimal.setVisibility(View.GONE);
}
}
private void populateLayout(ContentHolder holder, final Content content, int pos) {
attachTitle(holder, content);
attachCover(holder, content);
attachSeries(holder, content);
attachArtist(holder, content);
attachTags(holder, content);
attachSite(holder, content, pos);
}
private void attachTitle(ContentHolder holder, Content content) {
if (content.getTitle() == null) {
holder.tvTitle.setText(R.string.work_untitled);
if (holder.itemView.isSelected()) {
holder.tvTitle2.setText(R.string.work_untitled);
}
} else {
holder.tvTitle.setText(content.getTitle());
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) {
holder.tvTitle.setEllipsize(TextUtils.TruncateAt.MARQUEE);
holder.tvTitle.setSingleLine(true);
holder.tvTitle.setMarqueeRepeatLimit(5);
}
holder.tvTitle.setSelected(true);
if (holder.itemView.isSelected()) {
holder.tvTitle2.setText(content.getTitle());
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) {
holder.tvTitle2.setEllipsize(TextUtils.TruncateAt.MARQUEE);
holder.tvTitle2.setSingleLine(true);
holder.tvTitle2.setMarqueeRepeatLimit(5);
}
holder.tvTitle2.setSelected(true);
}
}
}
private void attachCover(ContentHolder holder, Content content) {
// The following is needed due to RecyclerView recycling layouts and
// Glide not considering the layout invalid for the current image:
// https://github.com/bumptech/glide/issues/835#issuecomment-167438903
holder.ivCover.layout(0, 0, 0, 0);
holder.ivCover2.layout(0, 0, 0, 0);
Glide.with(cxt)
.load(FileHelper.getThumb(cxt, content))
.fitCenter()
.placeholder(R.drawable.ic_placeholder)
.error(R.drawable.ic_placeholder)
.into(holder.ivCover);
if (holder.itemView.isSelected()) {
Glide.with(cxt)
.load(FileHelper.getThumb(cxt, content))
.fitCenter()
.placeholder(R.drawable.ic_placeholder)
.error(R.drawable.ic_placeholder)
.into(holder.ivCover2);
}
}
private void attachSeries(ContentHolder holder, Content content) {
String templateSeries = cxt.getResources().getString(R.string.work_series);
String series = "";
List<Attribute> seriesAttributes = content.getAttributes().get(AttributeType.SERIE);
if (seriesAttributes == null) {
holder.tvSeries.setVisibility(View.GONE);
} else {
for (int i = 0; i < seriesAttributes.size(); i++) {
Attribute attribute = seriesAttributes.get(i);
series += attribute.getName();
if (i != seriesAttributes.size() - 1) {
series += ", ";
}
}
holder.tvSeries.setVisibility(View.VISIBLE);
}
holder.tvSeries.setText(Helper.fromHtml(templateSeries.replace("@series@", series)));
if (seriesAttributes == null) {
holder.tvSeries.setText(Helper.fromHtml(templateSeries.replace("@series@",
cxt.getResources().getString(R.string.work_untitled))));
holder.tvSeries.setVisibility(View.VISIBLE);
}
}
private void attachArtist(ContentHolder holder, Content content) {
String templateArtist = cxt.getResources().getString(R.string.work_artist);
String artists = "";
List<Attribute> artistAttributes = content.getAttributes().get(AttributeType.ARTIST);
if (artistAttributes == null) {
holder.tvArtist.setVisibility(View.GONE);
} else {
for (int i = 0; i < artistAttributes.size(); i++) {
Attribute attribute = artistAttributes.get(i);
artists += attribute.getName();
if (i != artistAttributes.size() - 1) {
artists += ", ";
}
}
holder.tvArtist.setVisibility(View.VISIBLE);
}
holder.tvArtist.setText(Helper.fromHtml(templateArtist.replace("@artist@", artists)));
if (artistAttributes == null) {
holder.tvArtist.setText(Helper.fromHtml(templateArtist.replace("@artist@",
cxt.getResources().getString(R.string.work_untitled))));
holder.tvArtist.setVisibility(View.VISIBLE);
}
}
private void attachTags(ContentHolder holder, Content content) {
String templateTags = cxt.getResources().getString(R.string.work_tags);
String tags = "";
List<Attribute> tagsAttributes = content.getAttributes().get(AttributeType.TAG);
if (tagsAttributes != null) {
for (int i = 0; i < tagsAttributes.size(); i++) {
Attribute attribute = tagsAttributes.get(i);
if (attribute.getName() != null) {
tags += templateTags.replace("@tag@", attribute.getName());
if (i != tagsAttributes.size() - 1) {
tags += ", ";
}
}
}
}
holder.tvTags.setText(Helper.fromHtml(tags));
}
private void attachSite(ContentHolder holder, final Content content, int pos) {
if (content.getSite() != null) {
int img = content.getSite().getIco();
holder.ivSite.setImageResource(img);
holder.ivSite.setOnClickListener(v -> {
if (getSelectedItemCount() >= 1) {
clearSelections();
listener.onItemClear(0);
}
Helper.viewContent(cxt, content);
});
} else {
holder.ivSite.setImageResource(R.drawable.ic_stat_hentoid);
}
if (content.getStatus() != null) {
StatusContent status = content.getStatus();
int bg;
switch (status) {
case DOWNLOADED:
bg = R.color.card_item_src_normal;
break;
case MIGRATED:
bg = R.color.card_item_src_migrated;
break;
default:
LogHelper.d(TAG, "Position: " + pos + ": " + content.getTitle() +
" - Status: " + status);
bg = R.color.card_item_src_other;
break;
}
holder.ivSite.setBackgroundColor(ContextCompat.getColor(cxt, bg));
if (status == StatusContent.ERROR) {
holder.ivError.setVisibility(View.VISIBLE);
holder.ivError.setOnClickListener(v -> {
if (getSelectedItemCount() >= 1) {
clearSelections();
listener.onItemClear(0);
}
downloadAgain(content);
});
} else {
holder.ivError.setVisibility(View.GONE);
}
} else {
holder.ivSite.setVisibility(View.GONE);
}
}
private void attachOnClickListeners(final ContentHolder holder, Content content, int pos) {
holder.itemView.setOnClickListener(new ItemClickListener(cxt, content, pos, listener) {
@Override
public void onClick(View v) {
if (getSelectedItems() != null) {
int itemPos = holder.getLayoutPosition();
boolean selected = getSelectedItem(itemPos);
boolean selectionMode = getSelectedItemCount() > 0;
if (selectionMode) {
LogHelper.d(TAG, "In Selection Mode - ignore open requests.");
if (selected) {
LogHelper.d(TAG, "Item already selected, remove it.");
toggleSelection(itemPos);
setSelected(false, getSelectedItemCount());
} else {
LogHelper.d(TAG, "Item not selected, add it.");
toggleSelection(itemPos);
setSelected(true, getSelectedItemCount());
}
onLongClick(v);
} else {
LogHelper.d(TAG, "Not in selection mode, opening item.");
clearSelections();
setSelected(false, 0);
super.onClick(v);
}
}
}
});
holder.itemView.setOnLongClickListener(new ItemClickListener(cxt, content, pos, listener) {
@Override
public boolean onLongClick(View v) {
if (getSelectedItems() != null) {
int itemPos = holder.getLayoutPosition();
boolean selected = getSelectedItem(itemPos);
if (selected) {
LogHelper.d(TAG, "Item already selected, remove it.");
toggleSelection(itemPos);
setSelected(false, getSelectedItemCount());
} else {
LogHelper.d(TAG, "Item not selected, add it.");
toggleSelection(itemPos);
setSelected(true, getSelectedItemCount());
}
super.onLongClick(v);
return true;
}
return false;
}
});
}
private void downloadAgain(final Content item) {
int images;
int imgErrors = 0;
images = item.getImageFiles().size();
for (ImageFile imgFile : item.getImageFiles()) {
if (imgFile.getStatus() == StatusContent.ERROR) {
imgErrors++;
}
}
String message = cxt.getString(R.string.download_again_dialog_message).replace(
"@error", imgErrors + "").replace("@total", images + "");
AlertDialog.Builder builder = new AlertDialog.Builder(cxt);
builder.setTitle(R.string.download_again_dialog_title)
.setMessage(message)
.setPositiveButton(android.R.string.yes,
(dialog, which) -> {
HentoidDB db = HentoidDB.getInstance(cxt);
item.setStatus(StatusContent.DOWNLOADING);
item.setDownloadDate(new Date().getTime());
db.updateContentStatus(item);
Intent intent = new Intent(Intent.ACTION_SYNC, null, cxt,
DownloadService.class);
cxt.startService(intent);
Helper.toast(cxt, R.string.add_to_queue);
removeItem(item);
notifyDataSetChanged();
})
.setNegativeButton(android.R.string.no, null)
.create().show();
}
private void shareContent(final Content item) {
String url = item.getGalleryUrl();
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND);
intent.setData(Uri.parse(url));
intent.putExtra(Intent.EXTRA_SUBJECT, item.getTitle());
intent.putExtra(Intent.EXTRA_TEXT, url);
intent.setType("text/plain");
cxt.startActivity(Intent.createChooser(intent, cxt.getString(R.string.send_to)));
}
private void archiveContent(final Content item) {
Helper.toast(R.string.packaging_content);
FileHelper.archiveContent(cxt, item);
}
private void deleteContent(final Content item) {
AlertDialog.Builder builder = new AlertDialog.Builder(cxt);
builder.setMessage(R.string.ask_delete)
.setPositiveButton(android.R.string.yes,
(dialog, which) -> {
clearSelections();
deleteItem(item);
})
.setNegativeButton(android.R.string.no,
(dialog, which) -> {
clearSelections();
listener.onItemClear(0);
})
.create().show();
}
private void deleteContents(final List<Content> items) {
AlertDialog.Builder builder = new AlertDialog.Builder(cxt);
builder.setMessage(R.string.ask_delete_multiple)
.setPositiveButton(android.R.string.yes,
(dialog, which) -> {
clearSelections();
deleteItems(items);
})
.setNegativeButton(android.R.string.no,
(dialog, which) -> {
clearSelections();
listener.onItemClear(0);
})
.create().show();
}
@Override
public long getItemId(int position) {
return contents.get(position).getId();
}
@Override
public int getItemCount() {
return (isFooterEnabled) ? contents.size() + 1 : contents.size();
}
@Override
public int getItemViewType(int pos) {
return (isFooterEnabled && pos >= contents.size()) ? VIEW_TYPE_LOADING : VIEW_TYPE_ITEM;
}
public void enableFooter(boolean isEnabled) {
this.isFooterEnabled = isEnabled;
}
public void sharedSelectedItems() {
int itemCount = getSelectedItemCount();
if (itemCount > 0) {
if (itemCount == 1) {
LogHelper.d(TAG, "Preparing to share selected item...");
List<Content> items;
items = processSelection();
if (!items.isEmpty()) {
shareContent(items.get(0));
} else {
listener.onItemClear(0);
LogHelper.d(TAG, "Nothing to share!!");
}
} else {
// TODO: Implement multi-item share
LogHelper.d(TAG, "How even?");
Helper.toast("Not yet implemented!!");
}
} else {
listener.onItemClear(0);
LogHelper.d(TAG, "No items to share!!");
}
}
public void purgeSelectedItems() {
int itemCount = getSelectedItemCount();
if (itemCount > 0) {
if (itemCount == 1) {
LogHelper.d(TAG, "Preparing to delete selected item...");
List<Content> items;
items = processSelection();
if (!items.isEmpty()) {
deleteContent(items.get(0));
} else {
listener.onItemClear(0);
LogHelper.d(TAG, "Nothing to delete!!");
}
} else {
LogHelper.d(TAG, "Preparing to delete selected items...");
List<Content> items;
items = processSelection();
if (!items.isEmpty()) {
deleteContents(items);
} else {
listener.onItemClear(0);
LogHelper.d(TAG, "No items to delete!!");
}
}
} else {
listener.onItemClear(0);
LogHelper.d(TAG, "No items to delete!!");
}
}
public void archiveSelectedItems() {
int itemCount = getSelectedItemCount();
if (itemCount > 0) {
if (itemCount == 1) {
LogHelper.d(TAG, "Preparing to archive selected item...");
List<Content> items;
items = processSelection();
if (!items.isEmpty()) {
archiveContent(items.get(0));
} else {
listener.onItemClear(0);
LogHelper.d(TAG, "Nothing to archive!!");
}
} else {
// TODO: Implement multi-item archival
LogHelper.d(TAG, "How even?");
Helper.toast("Not yet implemented!!");
}
} else {
listener.onItemClear(0);
LogHelper.d(TAG, "No items to archive!!");
}
}
private List<Content> processSelection() {
List<Content> selectionList = new ArrayList<>();
List<Integer> selection = getSelectedItems();
LogHelper.d(TAG, "Selected items: " + selection);
for (int i = 0; i < selection.size(); i++) {
selectionList.add(i, contents.get(selection.get(i)));
LogHelper.d(TAG, "Added: " + contents.get(selection.get(i)).getTitle()
+ " to list.");
}
return selectionList;
}
private void removeItem(Content item) {
removeItem(item, true);
}
private void removeItem(Content item, boolean broadcast) {
int position = contents.indexOf(item);
LogHelper.d(TAG, "Removing item: " + item.getTitle() + " from adapter.");
contents.remove(position);
notifyItemRemoved(position);
if (contents != null) {
if (contents.size() == 0) {
contentsWipedListener.onContentsWiped();
}
if (broadcast) {
listener.onItemClear(0);
}
}
}
private void deleteItem(final Content item) {
final HentoidDB db = HentoidDB.getInstance(cxt);
removeItem(item);
AsyncTask.execute(() -> {
FileHelper.removeContent(cxt, item);
db.deleteContent(item);
LogHelper.d(TAG, "Removed item: " + item.getTitle() + " from db and file system.");
});
notifyDataSetChanged();
Helper.toast(cxt, cxt.getString(R.string.deleted).replace("@content", item.getTitle()));
}
private void deleteItems(final List<Content> items) {
final HentoidDB db = HentoidDB.getInstance(cxt);
for (int i = 0; i < items.size(); i++) {
removeItem(items.get(i), false);
}
AsyncTask.execute(() -> {
for (int i = 0; i < items.size(); i++) {
FileHelper.removeContent(cxt, items.get(i));
db.deleteContent(items.get(i));
LogHelper.d(TAG, "Removed item: " + items.get(i).getTitle()
+ " from db and file system.");
}
});
listener.onItemClear(0);
notifyDataSetChanged();
Helper.toast(cxt, "Selected items have been deleted.");
}
public interface EndlessScrollListener {
void onLoadMore();
}
public interface ContentsWipedListener {
void onContentsWiped();
}
private static class ProgressViewHolder extends RecyclerView.ViewHolder {
final ProgressBar progressBar;
ProgressViewHolder(View itemView) {
super(itemView);
progressBar = (ProgressBar) itemView.findViewById(R.id.loadingProgress);
}
}
}