Show image descriptions in gallery (#630)

* Add circleci

* Commit to maybe fix ci

* Suppress false positives in lint

* Disable linting for tests in ci

* Add image descriptions to gallery

* Fix test

* [CI] Attempt to fix OOM error

* [CI] Attempt to fix OOM error, 2

* Add option to open status from media

* fix theme issue

* increase linespacing on media description
This commit is contained in:
Ivan Kupalov 2018-05-10 21:13:25 +03:00 committed by Konrad Pozniak
parent 1108652823
commit 23d84dfa66
27 changed files with 313 additions and 193 deletions

View File

@ -1,4 +1,7 @@
version: 2 version: 2
machine:
environment:
GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError"'
jobs: jobs:
build: build:
working_directory: ~/code working_directory: ~/code

View File

@ -39,25 +39,58 @@ import android.support.v4.view.ViewPager;
import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBar;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.widget.Toast; import android.widget.Toast;
import com.keylesspalace.tusky.entity.Attachment;
import com.keylesspalace.tusky.fragment.ViewMediaFragment; import com.keylesspalace.tusky.fragment.ViewMediaFragment;
import com.keylesspalace.tusky.pager.ImagePagerAdapter; import com.keylesspalace.tusky.pager.ImagePagerAdapter;
import com.keylesspalace.tusky.view.ImageViewPager; import com.keylesspalace.tusky.view.ImageViewPager;
import com.keylesspalace.tusky.viewdata.AttachmentViewData;
import java.io.File; import java.io.File;
import java.util.ArrayList;
import java.util.List;
import kotlin.collections.CollectionsKt;
import kotlin.jvm.functions.Function0;
public final class ViewMediaActivity extends BaseActivity
implements ViewMediaFragment.PhotoActionsListener {
private static final String ATTACHMENTS_EXTRA = "attachments";
private static final String INDEX_EXTRA = "index";
public static Intent newIntent(Context context, List<AttachmentViewData> attachments,
int index) {
final Intent intent = new Intent(context, ViewMediaActivity.class);
intent.putParcelableArrayListExtra(ATTACHMENTS_EXTRA, new ArrayList<>(attachments));
intent.putExtra(INDEX_EXTRA, index);
return intent;
}
public class ViewMediaActivity extends BaseActivity implements ViewMediaFragment.PhotoActionsListener {
private static final int PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE = 1; private static final int PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE = 1;
private ImageViewPager viewPager; private ImageViewPager viewPager;
private View anyView; private View anyView;
private String[] imageUrls; private List<AttachmentViewData> attachments;
private Toolbar toolbar; private Toolbar toolbar;
private boolean isToolbarVisible = true; private boolean isToolbarVisible = true;
private final List<ToolbarVisibilityListener> toolbarVisibilityListeners = new ArrayList<>();
public interface ToolbarVisibilityListener {
void onToolbarVisiblityChanged(boolean isVisible);
}
public Function0 addToolbarVisibilityListener(ToolbarVisibilityListener listener) {
this.toolbarVisibilityListeners.add(listener);
listener.onToolbarVisiblityChanged(isToolbarVisible);
return () -> toolbarVisibilityListeners.remove(listener);
}
public boolean isToolbarVisible() {
return isToolbarVisible;
}
@Override @Override
protected void onCreate(@Nullable Bundle savedInstanceState) { protected void onCreate(@Nullable Bundle savedInstanceState) {
@ -73,12 +106,14 @@ public class ViewMediaActivity extends BaseActivity implements ViewMediaFragment
// Gather the parameters. // Gather the parameters.
Intent intent = getIntent(); Intent intent = getIntent();
imageUrls = intent.getStringArrayExtra("urls"); attachments = intent.getParcelableArrayListExtra(ATTACHMENTS_EXTRA);
int initialPosition = intent.getIntExtra("urlIndex", 0); int initialPosition = intent.getIntExtra(INDEX_EXTRA, 0);
List<Attachment> realAttachs =
CollectionsKt.map(attachments, AttachmentViewData::getAttachment);
// Setup the view pager. // Setup the view pager.
final ImagePagerAdapter adapter = new ImagePagerAdapter(getSupportFragmentManager(), final ImagePagerAdapter adapter = new ImagePagerAdapter(getSupportFragmentManager(),
imageUrls, initialPosition); realAttachs, initialPosition);
viewPager.setAdapter(adapter); viewPager.setAdapter(adapter);
viewPager.setCurrentItem(initialPosition); viewPager.setCurrentItem(initialPosition);
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@ -106,23 +141,18 @@ public class ViewMediaActivity extends BaseActivity implements ViewMediaFragment
actionBar.setDisplayShowHomeEnabled(true); actionBar.setDisplayShowHomeEnabled(true);
actionBar.setTitle(adapter.getPageTitle(initialPosition)); actionBar.setTitle(adapter.getPageTitle(initialPosition));
} }
toolbar.setNavigationOnClickListener(new View.OnClickListener() { toolbar.setNavigationOnClickListener(v -> supportFinishAfterTransition());
@Override toolbar.setOnMenuItemClickListener(item -> {
public void onClick(View v) {
supportFinishAfterTransition();
}
});
toolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
int id = item.getItemId(); int id = item.getItemId();
switch (id) { switch (id) {
case R.id.action_download: case R.id.action_download:
downloadImage(); downloadImage();
break; break;
case R.id.action_open_status:
onOpenStatus();
break;
} }
return true; return true;
}
}); });
View decorView = getWindow().getDecorView(); View decorView = getWindow().getDecorView();
@ -161,8 +191,12 @@ public class ViewMediaActivity extends BaseActivity implements ViewMediaFragment
@Override @Override
public void onPhotoTap() { public void onPhotoTap() {
isToolbarVisible = !isToolbarVisible; isToolbarVisible = !isToolbarVisible;
for (ToolbarVisibilityListener listener : toolbarVisibilityListeners) {
listener.onToolbarVisiblityChanged(isToolbarVisible);
}
final int visibility = isToolbarVisible ? View.VISIBLE : View.INVISIBLE; final int visibility = isToolbarVisible ? View.VISIBLE : View.INVISIBLE;
int alpha = isToolbarVisible ? 1 : 0; int alpha = isToolbarVisible ? 1 : 0;
toolbar.animate().alpha(alpha) toolbar.animate().alpha(alpha)
.setListener(new AnimatorListenerAdapter() { .setListener(new AnimatorListenerAdapter() {
@Override @Override
@ -184,12 +218,7 @@ public class ViewMediaActivity extends BaseActivity implements ViewMediaFragment
downloadImage(); downloadImage();
} else { } else {
doErrorDialog(R.string.error_media_download_permission, R.string.action_retry, doErrorDialog(R.string.error_media_download_permission, R.string.action_retry,
new View.OnClickListener() { v -> downloadImage());
@Override
public void onClick(View v) {
downloadImage();
}
});
} }
break; break;
} }
@ -214,7 +243,7 @@ public class ViewMediaActivity extends BaseActivity implements ViewMediaFragment
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE); PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE);
} else { } else {
String url = imageUrls[viewPager.getCurrentItem()]; String url = attachments.get(viewPager.getCurrentItem()).getAttachment().getUrl();
Uri uri = Uri.parse(url); Uri uri = Uri.parse(url);
String filename = new File(url).getName(); String filename = new File(url).getName();
@ -234,4 +263,10 @@ public class ViewMediaActivity extends BaseActivity implements ViewMediaFragment
downloadManager.enqueue(request); downloadManager.enqueue(request);
} }
} }
private void onOpenStatus() {
final AttachmentViewData attach = attachments.get(viewPager.getCurrentItem());
startActivity(ViewThreadActivity.startIntent(this, attach.getStatusId(),
attach.getStatusUrl()));
}
} }

View File

@ -15,6 +15,8 @@
package com.keylesspalace.tusky; package com.keylesspalace.tusky;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
@ -24,6 +26,7 @@ import android.support.v7.widget.Toolbar;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.fragment.ViewThreadFragment; import com.keylesspalace.tusky.fragment.ViewThreadFragment;
import com.keylesspalace.tusky.util.LinkHelper; import com.keylesspalace.tusky.util.LinkHelper;
@ -39,6 +42,20 @@ public class ViewThreadActivity extends BottomSheetActivity implements HasSuppor
public static final int REVEAL_BUTTON_REVEAL = 2; public static final int REVEAL_BUTTON_REVEAL = 2;
public static final int REVEAL_BUTTON_HIDE = 3; public static final int REVEAL_BUTTON_HIDE = 3;
public static Intent startIntent(Context context, String id, String url) {
Intent intent = new Intent(context, ViewThreadActivity.class);
intent.putExtra(ID_EXTRA, id);
intent.putExtra(URL_EXTRA, url);
return intent;
}
public static Intent startIntentFromStatus(Context context, Status status) {
return startIntent(context, status.getActionableId(), status.getActionableStatus().getUrl());
}
private static final String ID_EXTRA = "id";
private static final String URL_EXTRA = "url";
private int revealButtonState = REVEAL_BUTTON_HIDDEN; private int revealButtonState = REVEAL_BUTTON_HIDDEN;
@Inject @Inject
@ -60,7 +77,7 @@ public class ViewThreadActivity extends BottomSheetActivity implements HasSuppor
actionBar.setDisplayShowHomeEnabled(true); actionBar.setDisplayShowHomeEnabled(true);
} }
String id = getIntent().getStringExtra("id"); String id = getIntent().getStringExtra(ID_EXTRA);
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragment = ViewThreadFragment.newInstance(id); fragment = ViewThreadFragment.newInstance(id);
fragmentTransaction.replace(R.id.fragment_container, fragment); fragmentTransaction.replace(R.id.fragment_container, fragment);
@ -98,7 +115,7 @@ public class ViewThreadActivity extends BottomSheetActivity implements HasSuppor
return true; return true;
} }
case R.id.action_open_in_web: { case R.id.action_open_in_web: {
LinkHelper.openLink(getIntent().getStringExtra("url"), this); LinkHelper.openLink(getIntent().getStringExtra(URL_EXTRA), this);
return true; return true;
} }
case R.id.action_reveal: { case R.id.action_reveal: {

View File

@ -30,6 +30,7 @@ import com.keylesspalace.tusky.view.RoundedTransformation;
import com.keylesspalace.tusky.viewdata.StatusViewData; import com.keylesspalace.tusky.viewdata.StatusViewData;
import com.mikepenz.iconics.utils.Utils; import com.mikepenz.iconics.utils.Utils;
import com.squareup.picasso.Picasso; import com.squareup.picasso.Picasso;
import at.connyduck.sparkbutton.SparkButton; import at.connyduck.sparkbutton.SparkButton;
import at.connyduck.sparkbutton.SparkEventListener; import at.connyduck.sparkbutton.SparkEventListener;
@ -199,7 +200,7 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
favouriteButton.setChecked(favourited); favouriteButton.setChecked(favourited);
} }
private void setMediaPreviews(final Attachment[] attachments, boolean sensitive, private void setMediaPreviews(final List<Attachment> attachments, boolean sensitive,
final StatusActionListener listener, boolean showingContent) { final StatusActionListener listener, boolean showingContent) {
final ImageView[] previews = { final ImageView[] previews = {
mediaPreview0, mediaPreview1, mediaPreview2, mediaPreview3 mediaPreview0, mediaPreview1, mediaPreview2, mediaPreview3
@ -213,16 +214,11 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
ThemeUtils.getDrawableId(itemView.getContext(), R.attr.media_preview_unloaded_drawable, ThemeUtils.getDrawableId(itemView.getContext(), R.attr.media_preview_unloaded_drawable,
android.R.color.black); android.R.color.black);
final int n = Math.min(attachments.length, Status.MAX_MEDIA_ATTACHMENTS); final int n = Math.min(attachments.size(), Status.MAX_MEDIA_ATTACHMENTS);
final String[] urls = new String[n];
for (int i = 0; i < n; i++) {
urls[i] = attachments[i].getUrl();
}
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
String previewUrl = attachments[i].getPreviewUrl(); String previewUrl = attachments.get(i).getPreviewUrl();
String description = attachments[i].getDescription(); String description = attachments.get(i).getDescription();
if (TextUtils.isEmpty(description)) { if (TextUtils.isEmpty(description)) {
previews[i].setContentDescription(context.getString(R.string.action_view_media)); previews[i].setContentDescription(context.getString(R.string.action_view_media));
@ -243,24 +239,15 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
.into(previews[i]); .into(previews[i]);
} }
final Attachment.Type type = attachments[i].getType(); final Attachment.Type type = attachments.get(i).getType();
if (type == Attachment.Type.VIDEO | type == Attachment.Type.GIFV) { if (type == Attachment.Type.VIDEO | type == Attachment.Type.GIFV) {
overlays[i].setVisibility(View.VISIBLE); overlays[i].setVisibility(View.VISIBLE);
} else { } else {
overlays[i].setVisibility(View.GONE); overlays[i].setVisibility(View.GONE);
} }
if (urls[i] == null || urls[i].isEmpty()) {
previews[i].setOnClickListener(null);
} else {
final int urlIndex = i; final int urlIndex = i;
previews[i].setOnClickListener(new View.OnClickListener() { previews[i].setOnClickListener(v -> listener.onViewMedia(getAdapterPosition(), urlIndex, v));
@Override
public void onClick(View v) {
listener.onViewMedia(urls, urlIndex, type, v);
}
});
}
if (n <= 2) { if (n <= 2) {
previews[0].getLayoutParams().height = getMediaPreviewHeight(context) * 2; previews[0].getLayoutParams().height = getMediaPreviewHeight(context) * 2;
@ -289,25 +276,19 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
sensitiveMediaWarning.setVisibility(showingContent ? View.GONE : View.VISIBLE); sensitiveMediaWarning.setVisibility(showingContent ? View.GONE : View.VISIBLE);
sensitiveMediaShow.setVisibility(showingContent ? View.VISIBLE : View.GONE); sensitiveMediaShow.setVisibility(showingContent ? View.VISIBLE : View.GONE);
sensitiveMediaShow.setOnClickListener(new View.OnClickListener() { sensitiveMediaShow.setOnClickListener(v -> {
@Override
public void onClick(View v) {
if (getAdapterPosition() != RecyclerView.NO_POSITION) { if (getAdapterPosition() != RecyclerView.NO_POSITION) {
listener.onContentHiddenChange(false, getAdapterPosition()); listener.onContentHiddenChange(false, getAdapterPosition());
} }
v.setVisibility(View.GONE); v.setVisibility(View.GONE);
sensitiveMediaWarning.setVisibility(View.VISIBLE); sensitiveMediaWarning.setVisibility(View.VISIBLE);
}
}); });
sensitiveMediaWarning.setOnClickListener(new View.OnClickListener() { sensitiveMediaWarning.setOnClickListener(v -> {
@Override
public void onClick(View v) {
if (getAdapterPosition() != RecyclerView.NO_POSITION) { if (getAdapterPosition() != RecyclerView.NO_POSITION) {
listener.onContentHiddenChange(true, getAdapterPosition()); listener.onContentHiddenChange(true, getAdapterPosition());
} }
v.setVisibility(View.GONE); v.setVisibility(View.GONE);
sensitiveMediaShow.setVisibility(View.VISIBLE); sensitiveMediaShow.setVisibility(View.VISIBLE);
}
}); });
@ -341,9 +322,9 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
} }
} }
private void setMediaLabel(Attachment[] attachments, boolean sensitive, private void setMediaLabel(List<Attachment> attachments, boolean sensitive,
final StatusActionListener listener) { final StatusActionListener listener) {
if (attachments.length == 0) { if (attachments.size() == 0) {
mediaLabel.setVisibility(View.GONE); mediaLabel.setVisibility(View.GONE);
return; return;
} }
@ -351,7 +332,7 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
// Set the label's text. // Set the label's text.
Context context = itemView.getContext(); Context context = itemView.getContext();
String labelText = getLabelTypeText(context, attachments[0].getType()); String labelText = getLabelTypeText(context, attachments.get(0).getType());
if (sensitive) { if (sensitive) {
String sensitiveText = context.getString(R.string.status_sensitive_media_title); String sensitiveText = context.getString(R.string.status_sensitive_media_title);
labelText += String.format(" (%s)", sensitiveText); labelText += String.format(" (%s)", sensitiveText);
@ -359,24 +340,12 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
mediaLabel.setText(labelText); mediaLabel.setText(labelText);
// Set the icon next to the label. // Set the icon next to the label.
int drawableId = getLabelIcon(attachments[0].getType()); int drawableId = getLabelIcon(attachments.get(0).getType());
Drawable drawable = AppCompatResources.getDrawable(context, drawableId); Drawable drawable = AppCompatResources.getDrawable(context, drawableId);
ThemeUtils.setDrawableTint(context, drawable, android.R.attr.textColorTertiary); ThemeUtils.setDrawableTint(context, drawable, android.R.attr.textColorTertiary);
mediaLabel.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null); mediaLabel.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null);
// Set the listener for the media view action. mediaLabel.setOnClickListener(v -> listener.onViewMedia(getAdapterPosition(), 0, null));
int n = Math.min(attachments.length, Status.MAX_MEDIA_ATTACHMENTS);
final String[] urls = new String[n];
for (int i = 0; i < n; i++) {
urls[i] = attachments[i].getUrl();
}
final Attachment.Type type = attachments[0].getType();
mediaLabel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
listener.onViewMedia(urls, 0, type, null);
}
});
} }
private void hideSensitiveMediaWarning() { private void hideSensitiveMediaWarning() {
@ -483,14 +452,11 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
* if it contains URLSpans without also setting its listener. The surrounding spans will * if it contains URLSpans without also setting its listener. The surrounding spans will
* just eat the clicks instead of deferring to the parent listener, but WILL respond to a * just eat the clicks instead of deferring to the parent listener, but WILL respond to a
* listener directly on the TextView, for whatever reason. */ * listener directly on the TextView, for whatever reason. */
View.OnClickListener viewThreadListener = new View.OnClickListener() { View.OnClickListener viewThreadListener = v -> {
@Override
public void onClick(View v) {
int position = getAdapterPosition(); int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION) { if (position != RecyclerView.NO_POSITION) {
listener.onViewThread(position); listener.onViewThread(position);
} }
}
}; };
content.setOnClickListener(viewThreadListener); content.setOnClickListener(viewThreadListener);
container.setOnClickListener(viewThreadListener); container.setOnClickListener(viewThreadListener);
@ -506,12 +472,12 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
setAvatar(status.getAvatar(), status.getRebloggedAvatar()); setAvatar(status.getAvatar(), status.getRebloggedAvatar());
setReblogged(status.isReblogged()); setReblogged(status.isReblogged());
setFavourited(status.isFavourited()); setFavourited(status.isFavourited());
Attachment[] attachments = status.getAttachments(); List<Attachment> attachments = status.getAttachments();
boolean sensitive = status.isSensitive(); boolean sensitive = status.isSensitive();
if (mediaPreviewEnabled) { if (mediaPreviewEnabled) {
setMediaPreviews(attachments, sensitive, listener, status.isShowingContent()); setMediaPreviews(attachments, sensitive, listener, status.isShowingContent());
if (attachments.length == 0) { if (attachments.size() == 0) {
hideSensitiveMediaWarning(); hideSensitiveMediaWarning();
// videoIndicator.setVisibility(View.GONE); // videoIndicator.setVisibility(View.GONE);
} }

View File

@ -110,7 +110,7 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
content.setOnLongClickListener(longClickListener); content.setOnLongClickListener(longClickListener);
contentWarningDescription.setOnLongClickListener(longClickListener); contentWarningDescription.setOnLongClickListener(longClickListener);
if(status.getAttachments().length == 0 && status.getCard() != null && !TextUtils.isEmpty(status.getCard().getUrl())) { if(status.getAttachments().size() == 0 && status.getCard() != null && !TextUtils.isEmpty(status.getCard().getUrl())) {
final Card card = status.getCard(); final Card card = status.getCard();
cardView.setVisibility(View.VISIBLE); cardView.setVisibility(View.VISIBLE);
cardTitle.setText(card.getTitle()); cardTitle.setText(card.getTitle());

View File

@ -15,13 +15,16 @@
package com.keylesspalace.tusky.entity package com.keylesspalace.tusky.entity
import android.os.Parcelable
import com.google.gson.JsonDeserializationContext import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement import com.google.gson.JsonElement
import com.google.gson.JsonParseException import com.google.gson.JsonParseException
import com.google.gson.annotations.JsonAdapter import com.google.gson.annotations.JsonAdapter
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import kotlinx.android.parcel.Parcelize
@Parcelize
data class Attachment( data class Attachment(
var id: String, var id: String,
var url: String, var url: String,
@ -29,7 +32,7 @@ data class Attachment(
@SerializedName("text_url") val textUrl: String?, @SerializedName("text_url") val textUrl: String?,
var type: Type, var type: Type,
var description: String? var description: String?
) { ) : Parcelable {
@JsonAdapter(MediaTypeDeserializer::class) @JsonAdapter(MediaTypeDeserializer::class)
enum class Type { enum class Type {

View File

@ -36,7 +36,7 @@ data class Status(
var sensitive: Boolean, var sensitive: Boolean,
@SerializedName("spoiler_text") val spoilerText: String, @SerializedName("spoiler_text") val spoilerText: String,
val visibility: Visibility, val visibility: Visibility,
@SerializedName("media_attachments") var attachments: Array<Attachment>, @SerializedName("media_attachments") var attachments: List<Attachment>,
val mentions: Array<Mention>, val mentions: Array<Mention>,
val application: Application? val application: Application?
) { ) {

View File

@ -38,6 +38,7 @@ import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.ThemeUtils import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.view.SquareImageView import com.keylesspalace.tusky.view.SquareImageView
import com.keylesspalace.tusky.viewdata.AttachmentViewData
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
@ -74,7 +75,7 @@ class AccountMediaFragment : BaseFragment(), Injectable {
private var currentCall: Call<List<Status>>? = null private var currentCall: Call<List<Status>>? = null
private val statuses = mutableListOf<Status>() private val statuses = mutableListOf<Status>()
private var fetchingStatus = FetchingStatus.NOT_FETCHING private var fetchingStatus = FetchingStatus.NOT_FETCHING
lateinit private var swipeLayout: SwipeRefreshLayout private lateinit var swipeLayout: SwipeRefreshLayout
private val callback = object : Callback<List<Status>> { private val callback = object : Callback<List<Status>> {
override fun onFailure(call: Call<List<Status>>?, t: Throwable?) { override fun onFailure(call: Call<List<Status>>?, t: Throwable?) {
@ -90,9 +91,9 @@ class AccountMediaFragment : BaseFragment(), Injectable {
body?.let { fetched -> body?.let { fetched ->
statuses.addAll(0, fetched) statuses.addAll(0, fetched)
// flatMap requires iterable but I don't want to box each array into list // flatMap requires iterable but I don't want to box each array into list
val result = mutableListOf<Attachment>() val result = mutableListOf<AttachmentViewData>()
for (status in fetched) { for (status in fetched) {
result.addAll(status.attachments) result.addAll(AttachmentViewData.list(status))
} }
adapter.addTop(result) adapter.addTop(result)
} }
@ -114,9 +115,9 @@ class AccountMediaFragment : BaseFragment(), Injectable {
statuses.addAll(fetched) statuses.addAll(fetched)
Log.d(TAG, "now there are ${statuses.size} statuses") Log.d(TAG, "now there are ${statuses.size} statuses")
// flatMap requires iterable but I don't want to box each array into list // flatMap requires iterable but I don't want to box each array into list
val result = mutableListOf<Attachment>() val result = mutableListOf<AttachmentViewData>()
for (status in fetched) { for (status in fetched) {
result.addAll(status.attachments) result.addAll(AttachmentViewData.list(status))
} }
adapter.addBottom(result) adapter.addBottom(result)
} }
@ -193,17 +194,14 @@ class AccountMediaFragment : BaseFragment(), Injectable {
} }
} }
private fun viewMedia(items: List<Attachment>, currentIndex: Int, view: View?) { private fun viewMedia(items: List<AttachmentViewData>, currentIndex: Int, view: View?) {
val urls = items.map { it.url }.toTypedArray() val type = items[currentIndex].attachment.type
val type = items[currentIndex].type
when (type) { when (type) {
Attachment.Type.IMAGE -> { Attachment.Type.IMAGE -> {
val intent = Intent(context, ViewMediaActivity::class.java) val intent = ViewMediaActivity.newIntent(context, items, currentIndex)
intent.putExtra("urls", urls)
intent.putExtra("urlIndex", currentIndex)
if (view != null && activity != null) { if (view != null && activity != null) {
val url = urls[currentIndex] val url = items[currentIndex].attachment.url
ViewCompat.setTransitionName(view, url) ViewCompat.setTransitionName(view, url)
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(activity!!, view, url) val options = ActivityOptionsCompat.makeSceneTransitionAnimation(activity!!, view, url)
startActivity(intent, options.toBundle()) startActivity(intent, options.toBundle())
@ -213,7 +211,7 @@ class AccountMediaFragment : BaseFragment(), Injectable {
} }
Attachment.Type.GIFV, Attachment.Type.VIDEO -> { Attachment.Type.GIFV, Attachment.Type.VIDEO -> {
val intent = Intent(context, ViewVideoActivity::class.java) val intent = Intent(context, ViewVideoActivity::class.java)
intent.putExtra("url", urls[currentIndex]) intent.putExtra("url", items[currentIndex].attachment.url)
startActivity(intent) startActivity(intent)
} }
Attachment.Type.UNKNOWN -> { Attachment.Type.UNKNOWN -> {
@ -232,16 +230,16 @@ class AccountMediaFragment : BaseFragment(), Injectable {
var baseItemColor = Color.BLACK var baseItemColor = Color.BLACK
private val items = mutableListOf<Attachment>() private val items = mutableListOf<AttachmentViewData>()
private val itemBgBaseHSV = FloatArray(3) private val itemBgBaseHSV = FloatArray(3)
private val random = Random() private val random = Random()
fun addTop(newItems: List<Attachment>) { fun addTop(newItems: List<AttachmentViewData>) {
items.addAll(0, newItems) items.addAll(0, newItems)
notifyItemRangeInserted(0, newItems.size) notifyItemRangeInserted(0, newItems.size)
} }
fun addBottom(newItems: List<Attachment>) { fun addBottom(newItems: List<AttachmentViewData>) {
if (newItems.isEmpty()) return if (newItems.isEmpty()) return
val oldLen = items.size val oldLen = items.size
@ -268,7 +266,7 @@ class AccountMediaFragment : BaseFragment(), Injectable {
holder.imageView.setBackgroundColor(Color.HSVToColor(itemBgBaseHSV)) holder.imageView.setBackgroundColor(Color.HSVToColor(itemBgBaseHSV))
val item = items[position] val item = items[position]
Picasso.with(holder.imageView.context) Picasso.with(holder.imageView.context)
.load(item.previewUrl) .load(item.attachment.previewUrl)
.into(holder.imageView) .into(holder.imageView)
} }

View File

@ -365,9 +365,10 @@ public class NotificationsFragment extends SFragment implements
} }
@Override @Override
public void onViewMedia(String[] urls, int urlIndex, Attachment.Type type, public void onViewMedia(int position, int attachmentIndex, View view) {
View view) { Notification notification = notifications.get(position).getAsRightOrNull();
super.viewMedia(urls, urlIndex, type, view); if (notification == null || notification.getStatus() == null) return;
super.viewMedia(attachmentIndex, notification.getStatus(), view);
} }
@Override @Override

View File

@ -43,8 +43,10 @@ import com.keylesspalace.tusky.interfaces.AdapterItemRemover;
import com.keylesspalace.tusky.network.MastodonApi; import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.network.TimelineCases; import com.keylesspalace.tusky.network.TimelineCases;
import com.keylesspalace.tusky.util.HtmlUtils; import com.keylesspalace.tusky.util.HtmlUtils;
import com.keylesspalace.tusky.viewdata.AttachmentViewData;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set; import java.util.Set;
import javax.inject.Inject; import javax.inject.Inject;
@ -201,15 +203,17 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov
popup.show(); popup.show();
} }
protected void viewMedia(String[] urls, int urlIndex, Attachment.Type type, protected void viewMedia(int urlIndex, Status status, @Nullable View view) {
@Nullable View view) { final Status actionable = status.getActionableStatus();
final Attachment active = actionable.getAttachments().get(urlIndex);
Attachment.Type type = active.getType();
switch (type) { switch (type) {
case IMAGE: { case IMAGE: {
Intent intent = new Intent(getContext(), ViewMediaActivity.class); final List<AttachmentViewData> attachments = AttachmentViewData.list(actionable);
intent.putExtra("urls", urls); final Intent intent = ViewMediaActivity.newIntent(getContext(), attachments,
intent.putExtra("urlIndex", urlIndex); urlIndex);
if (view != null) { if (view != null) {
String url = urls[urlIndex]; String url = active.getUrl();
ViewCompat.setTransitionName(view, url); ViewCompat.setTransitionName(view, url);
ActivityOptionsCompat options = ActivityOptionsCompat options =
ActivityOptionsCompat.makeSceneTransitionAnimation(getActivity(), ActivityOptionsCompat.makeSceneTransitionAnimation(getActivity(),
@ -223,7 +227,7 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov
case GIFV: case GIFV:
case VIDEO: { case VIDEO: {
Intent intent = new Intent(getContext(), ViewVideoActivity.class); Intent intent = new Intent(getContext(), ViewVideoActivity.class);
intent.putExtra("url", urls[urlIndex]); intent.putExtra("url", active.getUrl());
startActivity(intent); startActivity(intent);
break; break;
} }

View File

@ -175,8 +175,9 @@ class SearchFragment : SFragment(), StatusActionListener, Injectable {
} }
} }
override fun onViewMedia(urls: Array<out String>?, index: Int, type: Attachment.Type?, view: View?) { override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) {
viewMedia(urls, index, type, view) val status = searchAdapter.getStatusAtPosition(position) ?: return
viewMedia(attachmentIndex, status, view)
} }
override fun onViewThread(position: Int) { override fun onViewThread(position: Int) {

View File

@ -443,9 +443,10 @@ public class TimelineFragment extends SFragment implements
} }
@Override @Override
public void onViewMedia(String[] urls, int urlIndex, Attachment.Type type, public void onViewMedia(int position, int attachmentIndex, View view) {
View view) { Status status = statuses.get(position).getAsRightOrNull();
super.viewMedia(urls, urlIndex, type, view); if (status == null) return;
super.viewMedia(attachmentIndex, status, view);
} }
@Override @Override

View File

@ -15,28 +15,39 @@
package com.keylesspalace.tusky.fragment; package com.keylesspalace.tusky.fragment;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.content.Context; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.ViewCompat; import android.support.v4.view.ViewCompat;
import android.text.TextUtils;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView;
import com.github.chrisbanes.photoview.OnOutsidePhotoTapListener;
import com.github.chrisbanes.photoview.OnSingleFlingListener;
import com.github.chrisbanes.photoview.PhotoView; import com.github.chrisbanes.photoview.PhotoView;
import com.github.chrisbanes.photoview.PhotoViewAttacher; import com.github.chrisbanes.photoview.PhotoViewAttacher;
import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.ViewMediaActivity;
import com.keylesspalace.tusky.entity.Attachment;
import com.squareup.picasso.Callback; import com.squareup.picasso.Callback;
import com.squareup.picasso.NetworkPolicy; import com.squareup.picasso.NetworkPolicy;
import com.squareup.picasso.Picasso; import com.squareup.picasso.Picasso;
public class ViewMediaFragment extends BaseFragment { import java.util.Objects;
import kotlin.jvm.functions.Function0;
public final class ViewMediaFragment extends BaseFragment {
public interface PhotoActionsListener { public interface PhotoActionsListener {
void onBringUp(); void onBringUp();
void onDismiss(); void onDismiss();
void onPhotoTap(); void onPhotoTap();
} }
@ -44,13 +55,20 @@ public class ViewMediaFragment extends BaseFragment {
private PhotoActionsListener photoActionsListener; private PhotoActionsListener photoActionsListener;
private View rootView; private View rootView;
private PhotoView photoView; private PhotoView photoView;
private TextView descriptionView;
private boolean showingDescription;
private boolean isDescriptionVisible;
private Function0 toolbarVisibiltyDisposable;
private static final String ARG_START_POSTPONED_TRANSITION = "startPostponedTransition"; private static final String ARG_START_POSTPONED_TRANSITION = "startPostponedTransition";
private static final String ATTACH_ARG = "attach";
public static ViewMediaFragment newInstance(String url, boolean shouldStartPostponedTransition) { public static ViewMediaFragment newInstance(@NonNull Attachment attachment,
boolean shouldStartPostponedTransition) {
Bundle arguments = new Bundle(); Bundle arguments = new Bundle();
ViewMediaFragment fragment = new ViewMediaFragment(); ViewMediaFragment fragment = new ViewMediaFragment();
arguments.putString("url", url); arguments.putParcelable(ATTACH_ARG, attachment);
arguments.putBoolean(ARG_START_POSTPONED_TRANSITION, shouldStartPostponedTransition); arguments.putBoolean(ARG_START_POSTPONED_TRANSITION, shouldStartPostponedTransition);
fragment.setArguments(arguments); fragment.setArguments(arguments);
@ -64,43 +82,42 @@ public class ViewMediaFragment extends BaseFragment {
} }
@Override @Override
public View onCreateView(LayoutInflater inflater, final ViewGroup container, public View onCreateView(@NonNull LayoutInflater inflater, final ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
rootView = inflater.inflate(R.layout.fragment_view_media, container, false); rootView = inflater.inflate(R.layout.fragment_view_media, container, false);
photoView = rootView.findViewById(R.id.view_media_image); photoView = rootView.findViewById(R.id.view_media_image);
descriptionView = rootView.findViewById(R.id.tv_media_description);
final Bundle arguments = getArguments(); final Bundle arguments = Objects.requireNonNull(getArguments(), "Empty arguments");
final String url = arguments.getString("url"); final Attachment attachment = arguments.getParcelable(ATTACH_ARG);
final String url = attachment.getUrl();
@Nullable final String description = attachment.getDescription();
descriptionView.setText(description);
showingDescription = !TextUtils.isEmpty(description);
isDescriptionVisible = showingDescription;
// Setting visibility without animations so it looks nice when you scroll images
//noinspection ConstantConditions
descriptionView.setVisibility(showingDescription
&& (((ViewMediaActivity) getActivity())).isToolbarVisible()
? View.VISIBLE : View.GONE);
attacher = new PhotoViewAttacher(photoView); attacher = new PhotoViewAttacher(photoView);
// Clicking outside the photo closes the viewer. // Clicking outside the photo closes the viewer.
attacher.setOnOutsidePhotoTapListener(new OnOutsidePhotoTapListener() { attacher.setOnOutsidePhotoTapListener(imageView -> photoActionsListener.onDismiss());
@Override
public void onOutsidePhotoTap(ImageView imageView) {
photoActionsListener.onDismiss();
}
});
attacher.setOnClickListener(new View.OnClickListener() { attacher.setOnClickListener(v -> onMediaTap());
@Override
public void onClick(View v) {
photoActionsListener.onPhotoTap();
}
});
/* A vertical swipe motion also closes the viewer. This is especially useful when the photo /* A vertical swipe motion also closes the viewer. This is especially useful when the photo
* mostly fills the screen so clicking outside is difficult. */ * mostly fills the screen so clicking outside is difficult. */
attacher.setOnSingleFlingListener(new OnSingleFlingListener() { attacher.setOnSingleFlingListener((e1, e2, velocityX, velocityY) -> {
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
if (Math.abs(velocityY) > Math.abs(velocityX)) { if (Math.abs(velocityY) > Math.abs(velocityX)) {
photoActionsListener.onDismiss(); photoActionsListener.onDismiss();
return true; return true;
} }
return false; return false;
}
}); });
ViewCompat.setTransitionName(photoView, url); ViewCompat.setTransitionName(photoView, url);
@ -150,9 +167,37 @@ public class ViewMediaFragment extends BaseFragment {
loadImageFromNetwork(url, photoView); loadImageFromNetwork(url, photoView);
} }
toolbarVisibiltyDisposable = ((ViewMediaActivity) getActivity())
.addToolbarVisibilityListener(this::onToolbarVisibilityChange);
return rootView; return rootView;
} }
@Override
public void onDestroyView() {
if (toolbarVisibiltyDisposable != null) toolbarVisibiltyDisposable.invoke();
super.onDestroyView();
}
private void onMediaTap() {
photoActionsListener.onPhotoTap();
}
private void onToolbarVisibilityChange(boolean visible) {
isDescriptionVisible = showingDescription && visible;
final int visibility = isDescriptionVisible ? View.VISIBLE : View.INVISIBLE;
int alpha = isDescriptionVisible ? 1 : 0;
descriptionView.animate().alpha(alpha)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
descriptionView.setVisibility(visibility);
animation.removeListener(this);
}
})
.start();
}
@Override @Override
public void onDetach() { public void onDetach() {
super.onDetach(); super.onDetach();

View File

@ -267,9 +267,9 @@ public final class ViewThreadFragment extends SFragment implements
} }
@Override @Override
public void onViewMedia(String[] urls, int urlIndex, Attachment.Type type, public void onViewMedia(int position, int attachmentIndex, View view) {
View view) { Status status = statuses.get(position);
super.viewMedia(urls, urlIndex, type, view); super.viewMedia(attachmentIndex, status, view);
} }
@Override @Override

View File

@ -24,7 +24,7 @@ public interface StatusActionListener extends LinkListener {
void onReblog(final boolean reblog, final int position); void onReblog(final boolean reblog, final int position);
void onFavourite(final boolean favourite, final int position); void onFavourite(final boolean favourite, final int position);
void onMore(View view, final int position); void onMore(View view, final int position);
void onViewMedia(String[] urls, int index, Attachment.Type type, View view); void onViewMedia(int position, int attachmentIndex, View view);
void onViewThread(int position); void onViewThread(int position);
void onOpenReblog(int position); void onOpenReblog(int position);
void onExpandedChange(boolean expanded, int position); void onExpandedChange(boolean expanded, int position);

View File

@ -4,24 +4,27 @@ import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter; import android.support.v4.app.FragmentPagerAdapter;
import com.keylesspalace.tusky.entity.Attachment;
import com.keylesspalace.tusky.fragment.ViewMediaFragment; import com.keylesspalace.tusky.fragment.ViewMediaFragment;
import java.util.List;
import java.util.Locale; import java.util.Locale;
public class ImagePagerAdapter extends FragmentPagerAdapter { public final class ImagePagerAdapter extends FragmentPagerAdapter {
private String[] urls;
private List<Attachment> attachments;
private int initialPosition; private int initialPosition;
public ImagePagerAdapter(FragmentManager fragmentManager, String[] urls, int initialPosition) { public ImagePagerAdapter(FragmentManager fragmentManager, List<Attachment> attachments, int initialPosition) {
super(fragmentManager); super(fragmentManager);
this.urls = urls; this.attachments = attachments;
this.initialPosition = initialPosition; this.initialPosition = initialPosition;
} }
@Override @Override
public Fragment getItem(int position) { public Fragment getItem(int position) {
if (position >= 0 && position < urls.length) { if (position >= 0 && position < attachments.size()) {
return ViewMediaFragment.newInstance(urls[position], position == initialPosition); return ViewMediaFragment.newInstance(attachments.get(position), position == initialPosition);
} else { } else {
return null; return null;
} }
@ -29,11 +32,11 @@ public class ImagePagerAdapter extends FragmentPagerAdapter {
@Override @Override
public int getCount() { public int getCount() {
return urls.length; return attachments.size();
} }
@Override @Override
public CharSequence getPageTitle(int position) { public CharSequence getPageTitle(int position) {
return String.format(Locale.getDefault(), "%d/%d", position + 1, urls.length); return String.format(Locale.getDefault(), "%d/%d", position + 1, attachments.size());
} }
} }

View File

@ -0,0 +1,23 @@
package com.keylesspalace.tusky.viewdata
import android.os.Parcelable
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Status
import kotlinx.android.parcel.Parcelize
@Parcelize
data class AttachmentViewData(
val attachment: Attachment,
val statusId: String,
val statusUrl: String
) : Parcelable {
companion object {
@JvmStatic
fun list(status: Status): List<AttachmentViewData> {
val actionable = status.actionableStatus
return actionable.attachments.map {
AttachmentViewData(it, actionable.id, actionable.url)
}
}
}
}

View File

@ -23,13 +23,14 @@ import com.keylesspalace.tusky.entity.Card;
import com.keylesspalace.tusky.entity.Emoji; import com.keylesspalace.tusky.entity.Emoji;
import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.entity.Status;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
/** /**
* Created by charlag on 11/07/2017. * Created by charlag on 11/07/2017.
* * <p>
* Class to represent data required to display either a notification or a placeholder. * Class to represent data required to display either a notification or a placeholder.
* It is either a {@link StatusViewData.Concrete} or a {@link StatusViewData.Placeholder}. * It is either a {@link StatusViewData.Concrete} or a {@link StatusViewData.Placeholder}.
*/ */
@ -47,7 +48,7 @@ public abstract class StatusViewData {
@Nullable @Nullable
private final String spoilerText; private final String spoilerText;
private final Status.Visibility visibility; private final Status.Visibility visibility;
private final Attachment[] attachments; private final List<Attachment> attachments;
@Nullable @Nullable
private final String rebloggedByUsername; private final String rebloggedByUsername;
@Nullable @Nullable
@ -74,7 +75,7 @@ public abstract class StatusViewData {
private final Card card; private final Card card;
public Concrete(String id, Spanned content, boolean reblogged, boolean favourited, public Concrete(String id, Spanned content, boolean reblogged, boolean favourited,
@Nullable String spoilerText, Status.Visibility visibility, Attachment[] attachments, @Nullable String spoilerText, Status.Visibility visibility, List<Attachment> attachments,
@Nullable String rebloggedByUsername, @Nullable String rebloggedAvatar, boolean sensitive, boolean isExpanded, @Nullable String rebloggedByUsername, @Nullable String rebloggedAvatar, boolean sensitive, boolean isExpanded,
boolean isShowingContent, String userFullName, String nickname, String avatar, boolean isShowingContent, String userFullName, String nickname, String avatar,
Date createdAt, int reblogsCount, int favouritesCount, @Nullable String inReplyToId, Date createdAt, int reblogsCount, int favouritesCount, @Nullable String inReplyToId,
@ -132,7 +133,7 @@ public abstract class StatusViewData {
return visibility; return visibility;
} }
public Attachment[] getAttachments() { public List<Attachment> getAttachments() {
return attachments; return attachments;
} }
@ -234,7 +235,7 @@ public abstract class StatusViewData {
private boolean favourited; private boolean favourited;
private String spoilerText; private String spoilerText;
private Status.Visibility visibility; private Status.Visibility visibility;
private Attachment[] attachments; private List<Attachment> attachments;
private String rebloggedByUsername; private String rebloggedByUsername;
private String rebloggedAvatar; private String rebloggedAvatar;
private boolean isSensitive; private boolean isSensitive;
@ -264,7 +265,7 @@ public abstract class StatusViewData {
favourited = viewData.favourited; favourited = viewData.favourited;
spoilerText = viewData.spoilerText; spoilerText = viewData.spoilerText;
visibility = viewData.visibility; visibility = viewData.visibility;
attachments = viewData.attachments == null ? null : viewData.attachments.clone(); attachments = viewData.attachments == null ? null : new ArrayList<>(viewData.attachments);
rebloggedByUsername = viewData.rebloggedByUsername; rebloggedByUsername = viewData.rebloggedByUsername;
rebloggedAvatar = viewData.rebloggedAvatar; rebloggedAvatar = viewData.rebloggedAvatar;
isSensitive = viewData.isSensitive; isSensitive = viewData.isSensitive;
@ -315,7 +316,7 @@ public abstract class StatusViewData {
return this; return this;
} }
public Builder setAttachments(Attachment[] attachments) { public Builder setAttachments(List<Attachment> attachments) {
this.attachments = attachments; this.attachments = attachments;
return this; return this;
} }

View File

@ -179,7 +179,7 @@
android:layout_gravity="top" android:layout_gravity="top"
android:background="@android:color/transparent" android:background="@android:color/transparent"
app:layout_collapseMode="pin" app:layout_collapseMode="pin"
app:popupTheme="?attr/account_toolbar_popup_theme" /> app:popupTheme="?attr/toolbar_popup_theme" />
</android.support.design.widget.CollapsingToolbarLayout> </android.support.design.widget.CollapsingToolbarLayout>

View File

@ -15,8 +15,8 @@
android:id="@+id/toolbar" android:id="@+id/toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:background="@color/toolbar_view_media"
android:theme="@style/AppTheme.Account.AppBarLayout" android:theme="@style/AppTheme.Account.AppBarLayout"
app:popupTheme="@style/AppTheme.Account.ToolbarPopupTheme.Dark" app:popupTheme="?attr/toolbar_popup_theme"/>
android:background="@color/toolbar_view_media" />
</FrameLayout> </FrameLayout>

View File

@ -1,9 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="center" android:layout_gravity="center"
android:clickable="true"> android:clickable="true"
android:focusable="true">
<com.github.chrisbanes.photoview.PhotoView <com.github.chrisbanes.photoview.PhotoView
android:id="@+id/view_media_image" android:id="@+id/view_media_image"
@ -18,4 +20,16 @@
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:layout_gravity="center" /> android:layout_gravity="center" />
<TextView
android:id="@+id/tv_media_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="#60000000"
android:lineSpacingMultiplier="1.1"
android:padding="8dp"
android:textAlignment="center"
android:textColor="#eee"
tools:text="Some media description" />
</RelativeLayout> </RelativeLayout>

View File

@ -6,4 +6,8 @@
android:icon="@drawable/ic_file_download_black_24dp" android:icon="@drawable/ic_file_download_black_24dp"
android:title="@string/dialog_download_image" android:title="@string/dialog_download_image"
app:showAsAction="always" /> app:showAsAction="always" />
<item
android:id="@+id/action_open_status"
android:title="@string/action_open_toot"
app:showAsAction="never" />
</menu> </menu>

View File

@ -41,7 +41,7 @@
<item name="account_header_background_color">@color/account_header_background_dark</item> <item name="account_header_background_color">@color/account_header_background_dark</item>
<item name="account_toolbar_icon_tint_uncollapsed">@color/toolbar_icon_dark</item> <item name="account_toolbar_icon_tint_uncollapsed">@color/toolbar_icon_dark</item>
<item name="account_toolbar_icon_tint_collapsed">@color/account_toolbar_icon_collapsed_dark</item> <item name="account_toolbar_icon_tint_collapsed">@color/account_toolbar_icon_collapsed_dark</item>
<item name="account_toolbar_popup_theme">@style/AppTheme.Account.ToolbarPopupTheme.Dark</item> <item name="toolbar_popup_theme">@style/AppTheme.Account.ToolbarPopupTheme.Dark</item>
<item name="compose_close_button_tint">@color/toolbar_icon_dark</item> <item name="compose_close_button_tint">@color/toolbar_icon_dark</item>
<item name="compose_media_button_disabled_tint">@color/compose_media_button_disabled_dark</item> <item name="compose_media_button_disabled_tint">@color/compose_media_button_disabled_dark</item>
<item name="compose_mention_color">@color/color_accent_dark</item> <item name="compose_mention_color">@color/color_accent_dark</item>

View File

@ -27,7 +27,7 @@
<attr name="account_header_background_color" format="reference|color" /> <attr name="account_header_background_color" format="reference|color" />
<attr name="account_toolbar_icon_tint_uncollapsed" format="reference|color" /> <attr name="account_toolbar_icon_tint_uncollapsed" format="reference|color" />
<attr name="account_toolbar_icon_tint_collapsed" format="reference|color" /> <attr name="account_toolbar_icon_tint_collapsed" format="reference|color" />
<attr name="account_toolbar_popup_theme" format="reference" /> <attr name="toolbar_popup_theme" format="reference" />
<attr name="compose_close_button_tint" format="reference|color" /> <attr name="compose_close_button_tint" format="reference|color" />
<attr name="compose_media_button_disabled_tint" format="reference|color" /> <attr name="compose_media_button_disabled_tint" format="reference|color" />
<attr name="compose_mention_color" format="reference|color" /> <attr name="compose_mention_color" format="reference|color" />

View File

@ -315,6 +315,7 @@
<string name="download_fonts">You\'ll need to download these emoji sets first</string> <string name="download_fonts">You\'ll need to download these emoji sets first</string>
<string name="performing_lookup_title">Performing lookup...</string> <string name="performing_lookup_title">Performing lookup...</string>
<string name="expand_collapse_all_statuses">Expand/Collapse all statuses</string> <string name="expand_collapse_all_statuses">Expand/Collapse all statuses</string>
<string name="action_open_toot">Open toot</string>
<string name="restart_required">App restart required</string> <string name="restart_required">App restart required</string>
<string name="restart_emoji">You\'ll need to restart Tusky in order to apply these changes</string> <string name="restart_emoji">You\'ll need to restart Tusky in order to apply these changes</string>
<string name="later">Later</string> <string name="later">Later</string>

View File

@ -74,7 +74,7 @@
<item name="account_header_background_color">@color/account_header_background_light</item> <item name="account_header_background_color">@color/account_header_background_light</item>
<item name="account_toolbar_icon_tint_uncollapsed">@color/toolbar_icon_dark</item> <!--Default to dark on purpose, because header backgrounds with gradients are always dark.--> <item name="account_toolbar_icon_tint_uncollapsed">@color/toolbar_icon_dark</item> <!--Default to dark on purpose, because header backgrounds with gradients are always dark.-->
<item name="account_toolbar_icon_tint_collapsed">@color/account_toolbar_icon_collapsed_light</item> <item name="account_toolbar_icon_tint_collapsed">@color/account_toolbar_icon_collapsed_light</item>
<item name="account_toolbar_popup_theme">@style/AppTheme.Account.ToolbarPopupTheme.Light</item> <item name="toolbar_popup_theme">@style/AppTheme.Account.ToolbarPopupTheme.Light</item>
<item name="compose_close_button_tint">@color/toolbar_icon_light</item> <item name="compose_close_button_tint">@color/toolbar_icon_light</item>
<item name="compose_media_button_disabled_tint">@color/compose_media_button_disabled_light</item> <item name="compose_media_button_disabled_tint">@color/compose_media_button_disabled_light</item>
<item name="compose_mention_color">@color/compose_mention_light</item> <item name="compose_mention_color">@color/compose_mention_light</item>

View File

@ -78,7 +78,7 @@ class BottomSheetActivityTest {
false, false,
"", "",
Status.Visibility.PUBLIC, Status.Visibility.PUBLIC,
arrayOf(), listOf(),
arrayOf(), arrayOf(),
null null
) )