* Use blurhash as image preview and as sensitive media cover, close #1571 * Fix focal point for blurhashes * Fix video indicator overlapping sensitive media indicator * Add a preference for blurhash * Add blurhash to report UI. * Introduce StatusDisplayOptions
This commit is contained in:
parent
2994af7091
commit
7623962a0d
32 changed files with 560 additions and 368 deletions
|
@ -56,7 +56,7 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference
|
|||
setDisplayShowHomeEnabled(true)
|
||||
}
|
||||
|
||||
val fragment: Fragment = when(intent.getIntExtra(EXTRA_PREFERENCE_TYPE, 0)) {
|
||||
val fragment: Fragment = when (intent.getIntExtra(EXTRA_PREFERENCE_TYPE, 0)) {
|
||||
GENERAL_PREFERENCES -> {
|
||||
setTitle(R.string.action_view_preferences)
|
||||
PreferencesFragment.newInstance()
|
||||
|
@ -128,7 +128,8 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference
|
|||
this.restartCurrentActivity()
|
||||
|
||||
}
|
||||
"statusTextSize", "absoluteTimeView", "showBotOverlay", "animateGifAvatars" -> {
|
||||
"statusTextSize", "absoluteTimeView", "showBotOverlay", "animateGifAvatars",
|
||||
"useBlurhash" -> {
|
||||
restartActivitiesOnExit = true
|
||||
}
|
||||
"language" -> {
|
||||
|
|
|
@ -33,6 +33,12 @@ import android.widget.ImageView;
|
|||
import android.widget.TextView;
|
||||
import android.widget.ToggleButton;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.text.BidiFormatter;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.entity.Account;
|
||||
import com.keylesspalace.tusky.entity.Emoji;
|
||||
|
@ -40,10 +46,11 @@ import com.keylesspalace.tusky.entity.Notification;
|
|||
import com.keylesspalace.tusky.interfaces.LinkListener;
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
||||
import com.keylesspalace.tusky.util.TimestampUtils;
|
||||
import com.keylesspalace.tusky.util.ImageLoadingHelper;
|
||||
import com.keylesspalace.tusky.util.LinkHelper;
|
||||
import com.keylesspalace.tusky.util.SmartLengthInputFilter;
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions;
|
||||
import com.keylesspalace.tusky.util.TimestampUtils;
|
||||
import com.keylesspalace.tusky.viewdata.NotificationViewData;
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||
import com.mikepenz.iconics.utils.Utils;
|
||||
|
@ -53,12 +60,6 @@ import java.util.Date;
|
|||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.text.BidiFormatter;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||
|
||||
public interface AdapterDataSource<T> {
|
||||
|
@ -78,28 +79,23 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
private static final InputFilter[] NO_INPUT_FILTER = new InputFilter[0];
|
||||
|
||||
private String accountId;
|
||||
private StatusDisplayOptions statusDisplayOptions;
|
||||
private StatusActionListener statusListener;
|
||||
private NotificationActionListener notificationActionListener;
|
||||
private boolean mediaPreviewEnabled;
|
||||
private boolean useAbsoluteTime;
|
||||
private boolean showBotOverlay;
|
||||
private boolean animateAvatar;
|
||||
private BidiFormatter bidiFormatter;
|
||||
private AdapterDataSource<NotificationViewData> dataSource;
|
||||
|
||||
public NotificationsAdapter(String accountId,
|
||||
AdapterDataSource<NotificationViewData> dataSource,
|
||||
StatusDisplayOptions statusDisplayOptions,
|
||||
StatusActionListener statusListener,
|
||||
NotificationActionListener notificationActionListener) {
|
||||
|
||||
this.accountId = accountId;
|
||||
this.dataSource = dataSource;
|
||||
this.statusDisplayOptions = statusDisplayOptions;
|
||||
this.statusListener = statusListener;
|
||||
this.notificationActionListener = notificationActionListener;
|
||||
mediaPreviewEnabled = true;
|
||||
useAbsoluteTime = false;
|
||||
showBotOverlay = true;
|
||||
animateAvatar = false;
|
||||
bidiFormatter = BidiFormatter.getInstance();
|
||||
}
|
||||
|
||||
|
@ -111,17 +107,17 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
case VIEW_TYPE_STATUS: {
|
||||
View view = inflater
|
||||
.inflate(R.layout.item_status, parent, false);
|
||||
return new StatusViewHolder(view, useAbsoluteTime);
|
||||
return new StatusViewHolder(view);
|
||||
}
|
||||
case VIEW_TYPE_STATUS_NOTIFICATION: {
|
||||
View view = inflater
|
||||
.inflate(R.layout.item_status_notification, parent, false);
|
||||
return new StatusNotificationViewHolder(view, useAbsoluteTime, animateAvatar);
|
||||
return new StatusNotificationViewHolder(view, statusDisplayOptions);
|
||||
}
|
||||
case VIEW_TYPE_FOLLOW: {
|
||||
View view = inflater
|
||||
.inflate(R.layout.item_follow, parent, false);
|
||||
return new FollowViewHolder(view, animateAvatar);
|
||||
return new FollowViewHolder(view, statusDisplayOptions);
|
||||
}
|
||||
case VIEW_TYPE_PLACEHOLDER: {
|
||||
View view = inflater
|
||||
|
@ -137,7 +133,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
Utils.convertDpToPx(parent.getContext(), 24)
|
||||
)
|
||||
);
|
||||
return new RecyclerView.ViewHolder(view) {};
|
||||
return new RecyclerView.ViewHolder(view) {
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -171,8 +168,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
StatusViewHolder holder = (StatusViewHolder) viewHolder;
|
||||
StatusViewData.Concrete status = concreteNotificaton.getStatusViewData();
|
||||
holder.setupWithStatus(status,
|
||||
statusListener, mediaPreviewEnabled, showBotOverlay, animateAvatar, payloadForHolder);
|
||||
if(concreteNotificaton.getType() == Notification.Type.POLL) {
|
||||
statusListener, statusDisplayOptions, payloadForHolder);
|
||||
if (concreteNotificaton.getType() == Notification.Type.POLL) {
|
||||
holder.setPollInfo(accountId.equals(concreteNotificaton.getAccount().getId()));
|
||||
} else {
|
||||
holder.hideStatusInfo();
|
||||
|
@ -202,7 +199,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
concreteNotificaton.getId());
|
||||
} else {
|
||||
if (payloadForHolder instanceof List)
|
||||
for (Object item : (List)payloadForHolder) {
|
||||
for (Object item : (List) payloadForHolder) {
|
||||
if (StatusBaseViewHolder.Key.KEY_CREATED.equals(item)) {
|
||||
holder.setCreatedAt(statusViewData.getCreatedAt());
|
||||
}
|
||||
|
@ -221,7 +218,6 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
default:
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -229,6 +225,20 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
return dataSource.getItemCount();
|
||||
}
|
||||
|
||||
public void setMediaPreviewEnabled(boolean mediaPreviewEnabled) {
|
||||
this.statusDisplayOptions = statusDisplayOptions.copy(
|
||||
statusDisplayOptions.animateAvatars(),
|
||||
mediaPreviewEnabled,
|
||||
statusDisplayOptions.useAbsoluteTime(),
|
||||
statusDisplayOptions.showBotOverlay(),
|
||||
statusDisplayOptions.useBlurhash()
|
||||
);
|
||||
}
|
||||
|
||||
public boolean isMediaPreviewEnabled() {
|
||||
return this.statusDisplayOptions.mediaPreviewEnabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
NotificationViewData notification = dataSource.getItemAt(position);
|
||||
|
@ -256,26 +266,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
throw new AssertionError("Unknown notification type");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void setMediaPreviewEnabled(boolean enabled) {
|
||||
mediaPreviewEnabled = enabled;
|
||||
}
|
||||
|
||||
public boolean isMediaPreviewEnabled() {
|
||||
return mediaPreviewEnabled;
|
||||
}
|
||||
|
||||
public void setUseAbsoluteTime(boolean useAbsoluteTime) {
|
||||
this.useAbsoluteTime = useAbsoluteTime;
|
||||
}
|
||||
|
||||
public void setShowBotOverlay(boolean showBotOverlay) {
|
||||
this.showBotOverlay = showBotOverlay;
|
||||
}
|
||||
|
||||
public void setAnimateAvatar(boolean animateAvatar) {
|
||||
this.animateAvatar = animateAvatar;
|
||||
}
|
||||
|
||||
public interface NotificationActionListener {
|
||||
|
@ -300,15 +291,15 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
private TextView usernameView;
|
||||
private TextView displayNameView;
|
||||
private ImageView avatar;
|
||||
private boolean animateAvatar;
|
||||
private StatusDisplayOptions statusDisplayOptions;
|
||||
|
||||
FollowViewHolder(View itemView, boolean animateAvatar) {
|
||||
FollowViewHolder(View itemView, StatusDisplayOptions statusDisplayOptions) {
|
||||
super(itemView);
|
||||
message = itemView.findViewById(R.id.notification_text);
|
||||
usernameView = itemView.findViewById(R.id.notification_username);
|
||||
displayNameView = itemView.findViewById(R.id.notification_display_name);
|
||||
avatar = itemView.findViewById(R.id.notification_avatar);
|
||||
this.animateAvatar = animateAvatar;
|
||||
this.statusDisplayOptions = statusDisplayOptions;
|
||||
}
|
||||
|
||||
void setMessage(Account account, BidiFormatter bidiFormatter) {
|
||||
|
@ -330,7 +321,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
int avatarRadius = avatar.getContext().getResources()
|
||||
.getDimensionPixelSize(R.dimen.avatar_radius_42dp);
|
||||
|
||||
ImageLoadingHelper.loadAvatar(account.getAvatar(), avatar, avatarRadius, animateAvatar);
|
||||
ImageLoadingHelper.loadAvatar(account.getAvatar(), avatar, avatarRadius,
|
||||
statusDisplayOptions.animateAvatars());
|
||||
|
||||
}
|
||||
|
||||
|
@ -352,18 +344,16 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
private final TextView contentWarningDescriptionTextView;
|
||||
private final ToggleButton contentWarningButton;
|
||||
private final ToggleButton contentCollapseButton; // TODO: This code SHOULD be based on StatusBaseViewHolder
|
||||
private StatusDisplayOptions statusDisplayOptions;
|
||||
|
||||
private String accountId;
|
||||
private String notificationId;
|
||||
private NotificationActionListener notificationActionListener;
|
||||
private StatusViewData.Concrete statusViewData;
|
||||
|
||||
private boolean useAbsoluteTime;
|
||||
private boolean animateAvatar;
|
||||
private SimpleDateFormat shortSdf;
|
||||
private SimpleDateFormat longSdf;
|
||||
|
||||
StatusNotificationViewHolder(View itemView, boolean useAbsoluteTime, boolean animateAvatar) {
|
||||
StatusNotificationViewHolder(View itemView, StatusDisplayOptions statusDisplayOptions) {
|
||||
super(itemView);
|
||||
message = itemView.findViewById(R.id.notification_top_text);
|
||||
statusNameBar = itemView.findViewById(R.id.status_name_bar);
|
||||
|
@ -376,6 +366,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
contentWarningDescriptionTextView = itemView.findViewById(R.id.notification_content_warning_description);
|
||||
contentWarningButton = itemView.findViewById(R.id.notification_content_warning_button);
|
||||
contentCollapseButton = itemView.findViewById(R.id.button_toggle_notification_content);
|
||||
this.statusDisplayOptions = statusDisplayOptions;
|
||||
|
||||
int darkerFilter = Color.rgb(123, 123, 123);
|
||||
statusAvatar.setColorFilter(darkerFilter, PorterDuff.Mode.MULTIPLY);
|
||||
|
@ -385,9 +376,6 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
message.setOnClickListener(this);
|
||||
statusContent.setOnClickListener(this);
|
||||
contentWarningButton.setOnCheckedChangeListener(this);
|
||||
|
||||
this.useAbsoluteTime = useAbsoluteTime;
|
||||
this.animateAvatar = animateAvatar;
|
||||
shortSdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
|
||||
longSdf = new SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault());
|
||||
}
|
||||
|
@ -414,7 +402,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
}
|
||||
|
||||
protected void setCreatedAt(@Nullable Date createdAt) {
|
||||
if (useAbsoluteTime) {
|
||||
if (statusDisplayOptions.useAbsoluteTime()) {
|
||||
String time;
|
||||
if (createdAt != null) {
|
||||
if (System.currentTimeMillis() - createdAt.getTime() > 86400000L) {
|
||||
|
@ -511,13 +499,13 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
.getDimensionPixelSize(R.dimen.avatar_radius_36dp);
|
||||
|
||||
ImageLoadingHelper.loadAvatar(statusAvatarUrl,
|
||||
statusAvatar, statusAvatarRadius, animateAvatar);
|
||||
statusAvatar, statusAvatarRadius, statusDisplayOptions.animateAvatars());
|
||||
|
||||
int notificationAvatarRadius = statusAvatar.getContext().getResources()
|
||||
.getDimensionPixelSize(R.dimen.avatar_radius_24dp);
|
||||
|
||||
ImageLoadingHelper.loadAvatar(notificationAvatarUrl,
|
||||
notificationAvatar, notificationAvatarRadius, animateAvatar);
|
||||
ImageLoadingHelper.loadAvatar(notificationAvatarUrl, notificationAvatar,
|
||||
notificationAvatarRadius, statusDisplayOptions.animateAvatars());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package com.keylesspalace.tusky.adapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
|
@ -32,6 +34,7 @@ import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
|||
import com.keylesspalace.tusky.util.HtmlUtils;
|
||||
import com.keylesspalace.tusky.util.ImageLoadingHelper;
|
||||
import com.keylesspalace.tusky.util.LinkHelper;
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions;
|
||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
||||
import com.keylesspalace.tusky.util.TimestampUtils;
|
||||
import com.keylesspalace.tusky.view.MediaPreviewImageView;
|
||||
|
@ -49,7 +52,6 @@ import java.util.Locale;
|
|||
import java.util.Objects;
|
||||
|
||||
import at.connyduck.sparkbutton.SparkButton;
|
||||
import at.connyduck.sparkbutton.SparkEventListener;
|
||||
import kotlin.collections.CollectionsKt;
|
||||
|
||||
import static com.keylesspalace.tusky.viewdata.PollViewDataKt.buildDescription;
|
||||
|
@ -95,10 +97,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
private int avatarRadius36dp;
|
||||
private int avatarRadius24dp;
|
||||
|
||||
private final int mediaPreviewUnloadedId;
|
||||
private final Drawable mediaPreviewUnloaded;
|
||||
|
||||
protected StatusBaseViewHolder(View itemView,
|
||||
boolean useAbsoluteTime) {
|
||||
protected StatusBaseViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
displayName = itemView.findViewById(R.id.status_display_name);
|
||||
username = itemView.findViewById(R.id.status_username);
|
||||
|
@ -152,8 +153,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
this.avatarRadius36dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_36dp);
|
||||
this.avatarRadius24dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_24dp);
|
||||
|
||||
mediaPreviewUnloadedId = ThemeUtils.getDrawableId(itemView.getContext(),
|
||||
R.attr.media_preview_unloaded_drawable, android.R.color.black);
|
||||
mediaPreviewUnloaded = itemView.getContext().getDrawable(
|
||||
ThemeUtils.getDrawableId(itemView.getContext(),
|
||||
R.attr.media_preview_unloaded_drawable, android.R.color.black)
|
||||
);
|
||||
}
|
||||
|
||||
protected abstract int getMediaPreviewHeight(Context context);
|
||||
|
@ -234,14 +237,13 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
private void setAvatar(String url,
|
||||
@Nullable String rebloggedUrl,
|
||||
boolean isBot,
|
||||
boolean showBotOverlay,
|
||||
boolean animateAvatar) {
|
||||
StatusDisplayOptions statusDisplayOptions) {
|
||||
|
||||
int avatarRadius;
|
||||
if (TextUtils.isEmpty(rebloggedUrl)) {
|
||||
avatar.setPaddingRelative(0, 0, 0, 0);
|
||||
|
||||
if (showBotOverlay && isBot) {
|
||||
if (statusDisplayOptions.showBotOverlay() && isBot) {
|
||||
avatarInset.setVisibility(View.VISIBLE);
|
||||
avatarInset.setBackgroundColor(0x50ffffff);
|
||||
Glide.with(avatarInset)
|
||||
|
@ -260,12 +262,14 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
|
||||
avatarInset.setVisibility(View.VISIBLE);
|
||||
avatarInset.setBackground(null);
|
||||
ImageLoadingHelper.loadAvatar(rebloggedUrl, avatarInset, avatarRadius24dp, animateAvatar);
|
||||
ImageLoadingHelper.loadAvatar(rebloggedUrl, avatarInset, avatarRadius24dp,
|
||||
statusDisplayOptions.animateAvatars());
|
||||
|
||||
avatarRadius = avatarRadius36dp;
|
||||
}
|
||||
|
||||
ImageLoadingHelper.loadAvatar(url, avatar, avatarRadius, animateAvatar);
|
||||
ImageLoadingHelper.loadAvatar(url, avatar, avatarRadius,
|
||||
statusDisplayOptions.animateAvatars());
|
||||
|
||||
}
|
||||
|
||||
|
@ -273,7 +277,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
if (useAbsoluteTime) {
|
||||
timestampInfo.setText(getAbsoluteTime(createdAt));
|
||||
} else {
|
||||
if(createdAt == null) {
|
||||
if (createdAt == null) {
|
||||
timestampInfo.setText("?m");
|
||||
} else {
|
||||
long then = createdAt.getTime();
|
||||
|
@ -285,7 +289,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
}
|
||||
|
||||
private String getAbsoluteTime(Date createdAt) {
|
||||
if(createdAt == null) {
|
||||
if (createdAt == null) {
|
||||
return "??:??:??";
|
||||
}
|
||||
if (DateUtils.isToday(createdAt.getTime())) {
|
||||
|
@ -302,7 +306,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
/* This one is for screen-readers. Frequently, they would mispronounce timestamps like "17m"
|
||||
* as 17 meters instead of minutes. */
|
||||
|
||||
if(createdAt == null) {
|
||||
if (createdAt == null) {
|
||||
return "? minutes";
|
||||
} else {
|
||||
long then = createdAt.getTime();
|
||||
|
@ -367,12 +371,22 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
bookmarkButton.setChecked(bookmarked);
|
||||
}
|
||||
|
||||
private void loadImage(MediaPreviewImageView imageView, String previewUrl, MetaData meta) {
|
||||
private BitmapDrawable decodeBlurHash(String blurhash) {
|
||||
return ImageLoadingHelper.decodeBlurHash(this.avatar.getContext(), blurhash);
|
||||
}
|
||||
|
||||
private void loadImage(MediaPreviewImageView imageView, String previewUrl, MetaData meta,
|
||||
@Nullable String blurhash) {
|
||||
Drawable placeholder = blurhash != null ? decodeBlurHash(blurhash) : mediaPreviewUnloaded;
|
||||
if (TextUtils.isEmpty(previewUrl)) {
|
||||
if (blurhash != null) {
|
||||
imageView.setImageDrawable(decodeBlurHash(blurhash));
|
||||
} else {
|
||||
Glide.with(imageView)
|
||||
.load(mediaPreviewUnloadedId)
|
||||
.load(placeholder)
|
||||
.centerInside()
|
||||
.into(imageView);
|
||||
}
|
||||
} else {
|
||||
Focus focus = meta != null ? meta.getFocus() : null;
|
||||
|
||||
|
@ -381,7 +395,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
|
||||
Glide.with(imageView)
|
||||
.load(previewUrl)
|
||||
.placeholder(mediaPreviewUnloadedId)
|
||||
.placeholder(placeholder)
|
||||
.centerInside()
|
||||
.addListener(imageView)
|
||||
.into(imageView);
|
||||
|
@ -390,7 +404,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
|
||||
Glide.with(imageView)
|
||||
.load(previewUrl)
|
||||
.placeholder(mediaPreviewUnloadedId)
|
||||
.placeholder(placeholder)
|
||||
.centerInside()
|
||||
.into(imageView);
|
||||
}
|
||||
|
@ -398,39 +412,11 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
}
|
||||
|
||||
protected void setMediaPreviews(final List<Attachment> attachments, boolean sensitive,
|
||||
final StatusActionListener listener, boolean showingContent) {
|
||||
final StatusActionListener listener, boolean showingContent,
|
||||
boolean useBlurhash) {
|
||||
Context context = itemView.getContext();
|
||||
final int n = Math.min(attachments.size(), Status.MAX_MEDIA_ATTACHMENTS);
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
String previewUrl = attachments.get(i).getPreviewUrl();
|
||||
String description = attachments.get(i).getDescription();
|
||||
MediaPreviewImageView imageView = mediaPreviews[i];
|
||||
|
||||
imageView.setVisibility(View.VISIBLE);
|
||||
|
||||
if (TextUtils.isEmpty(description)) {
|
||||
imageView.setContentDescription(imageView.getContext()
|
||||
.getString(R.string.action_view_media));
|
||||
} else {
|
||||
imageView.setContentDescription(description);
|
||||
}
|
||||
|
||||
if (!sensitive || showingContent) {
|
||||
loadImage(imageView, previewUrl, attachments.get(i).getMeta());
|
||||
} else {
|
||||
imageView.setImageResource(mediaPreviewUnloadedId);
|
||||
}
|
||||
|
||||
final Attachment.Type type = attachments.get(i).getType();
|
||||
if (type == Attachment.Type.VIDEO || type == Attachment.Type.GIFV) {
|
||||
mediaOverlays[i].setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mediaOverlays[i].setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
setAttachmentClickListener(imageView, listener, i, attachments.get(i), true);
|
||||
}
|
||||
|
||||
final int mediaPreviewHeight = getMediaPreviewHeight(context);
|
||||
|
||||
|
@ -444,15 +430,51 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
mediaPreviews[3].getLayoutParams().height = mediaPreviewHeight;
|
||||
}
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
Attachment attachment = attachments.get(i);
|
||||
String previewUrl = attachment.getPreviewUrl();
|
||||
String description = attachment.getDescription();
|
||||
MediaPreviewImageView imageView = mediaPreviews[i];
|
||||
|
||||
imageView.setVisibility(View.VISIBLE);
|
||||
|
||||
if (TextUtils.isEmpty(description)) {
|
||||
imageView.setContentDescription(imageView.getContext()
|
||||
.getString(R.string.action_view_media));
|
||||
} else {
|
||||
imageView.setContentDescription(description);
|
||||
}
|
||||
|
||||
if (showingContent) {
|
||||
loadImage(imageView, previewUrl, attachment.getMeta(), attachment.getBlurhash());
|
||||
} else {
|
||||
imageView.setFocalPoint(null);
|
||||
imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
|
||||
if (useBlurhash && attachment.getBlurhash() != null) {
|
||||
BitmapDrawable blurhashBitmap = decodeBlurHash(attachment.getBlurhash());
|
||||
imageView.setImageDrawable(blurhashBitmap);
|
||||
} else {
|
||||
imageView.setImageDrawable(new ColorDrawable(ThemeUtils.getColor(
|
||||
context, R.attr.sensitive_media_warning_background_color)));
|
||||
}
|
||||
}
|
||||
|
||||
final Attachment.Type type = attachment.getType();
|
||||
if (showingContent && (type == Attachment.Type.VIDEO || type == Attachment.Type.GIFV)) {
|
||||
mediaOverlays[i].setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mediaOverlays[i].setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
setAttachmentClickListener(imageView, listener, i, attachment, true);
|
||||
}
|
||||
|
||||
|
||||
final String hiddenContentText;
|
||||
if (sensitive) {
|
||||
hiddenContentText = context.getString(R.string.status_sensitive_media_template,
|
||||
context.getString(R.string.status_sensitive_media_title),
|
||||
context.getString(R.string.status_sensitive_media_directions));
|
||||
hiddenContentText = context.getString(R.string.status_sensitive_media_title);
|
||||
} else {
|
||||
hiddenContentText = context.getString(R.string.status_sensitive_media_template,
|
||||
context.getString(R.string.status_media_hidden_title),
|
||||
context.getString(R.string.status_sensitive_media_directions));
|
||||
hiddenContentText = context.getString(R.string.status_media_hidden_title);
|
||||
}
|
||||
|
||||
sensitiveMediaWarning.setText(HtmlUtils.fromHtml(hiddenContentText));
|
||||
|
@ -528,8 +550,12 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
view.setOnClickListener(v -> {
|
||||
int position = getAdapterPosition();
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
if (sensitiveMediaWarning.getVisibility() == View.VISIBLE) {
|
||||
listener.onContentHiddenChange(true, getAdapterPosition());
|
||||
} else {
|
||||
listener.onViewMedia(position, index, animateTransition ? v : null);
|
||||
}
|
||||
}
|
||||
});
|
||||
view.setOnLongClickListener(v -> {
|
||||
CharSequence description = getAttachmentDescription(view.getContext(), attachment);
|
||||
|
@ -540,7 +566,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
|
||||
private static CharSequence getAttachmentDescription(Context context, Attachment attachment) {
|
||||
String duration = "";
|
||||
if(attachment.getMeta() != null && attachment.getMeta().getDuration() != null && attachment.getMeta().getDuration() > 0) {
|
||||
if (attachment.getMeta() != null && attachment.getMeta().getDuration() != null && attachment.getMeta().getDuration() > 0) {
|
||||
duration = formatDuration(attachment.getMeta().getDuration()) + " ";
|
||||
}
|
||||
if (TextUtils.isEmpty(attachment.getDescription())) {
|
||||
|
@ -608,29 +634,27 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
}
|
||||
|
||||
public void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener,
|
||||
boolean mediaPreviewEnabled, boolean showBotOverlay, boolean animateAvatar) {
|
||||
this.setupWithStatus(status, listener, mediaPreviewEnabled, showBotOverlay, animateAvatar, null);
|
||||
StatusDisplayOptions statusDisplayOptions) {
|
||||
this.setupWithStatus(status, listener, statusDisplayOptions, null);
|
||||
}
|
||||
|
||||
protected void setupWithStatus(StatusViewData.Concrete status,
|
||||
final StatusActionListener listener,
|
||||
boolean mediaPreviewEnabled,
|
||||
boolean showBotOverlay,
|
||||
boolean animateAvatar,
|
||||
StatusDisplayOptions statusDisplayOptions,
|
||||
@Nullable Object payloads) {
|
||||
if (payloads == null) {
|
||||
setDisplayName(status.getUserFullName(), status.getAccountEmojis());
|
||||
setUsername(status.getNickname());
|
||||
setCreatedAt(status.getCreatedAt());
|
||||
setIsReply(status.getInReplyToId() != null);
|
||||
setAvatar(status.getAvatar(), status.getRebloggedAvatar(), status.isBot(), showBotOverlay, animateAvatar);
|
||||
setAvatar(status.getAvatar(), status.getRebloggedAvatar(), status.isBot(), statusDisplayOptions);
|
||||
setReblogged(status.isReblogged());
|
||||
setFavourited(status.isFavourited());
|
||||
setBookmarked(status.isBookmarked());
|
||||
List<Attachment> attachments = status.getAttachments();
|
||||
boolean sensitive = status.isSensitive();
|
||||
if (mediaPreviewEnabled && !hasAudioAttachment(attachments)) {
|
||||
setMediaPreviews(attachments, sensitive, listener, status.isShowingContent());
|
||||
if (statusDisplayOptions.mediaPreviewEnabled() && !hasAudioAttachment(attachments)) {
|
||||
setMediaPreviews(attachments, sensitive, listener, status.isShowingContent(), statusDisplayOptions.useBlurhash());
|
||||
|
||||
if (attachments.size() == 0) {
|
||||
hideSensitiveMediaWarning();
|
||||
|
@ -674,7 +698,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
}
|
||||
|
||||
protected static boolean hasAudioAttachment(List<Attachment> attachments) {
|
||||
for(Attachment attachment: attachments) {
|
||||
for (Attachment attachment : attachments) {
|
||||
if (attachment.getType() == Attachment.Type.AUDIO) {
|
||||
return true;
|
||||
}
|
||||
|
@ -747,7 +771,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
|
||||
private static CharSequence getVisibilityDescription(Context context, Status.Visibility visibility) {
|
||||
|
||||
if(visibility == null) {
|
||||
if (visibility == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,9 @@ import android.widget.LinearLayout;
|
|||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
|
||||
import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners;
|
||||
|
@ -25,14 +28,12 @@ import com.keylesspalace.tusky.entity.Status;
|
|||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||
import com.keylesspalace.tusky.util.CustomURLSpan;
|
||||
import com.keylesspalace.tusky.util.LinkHelper;
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions;
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
||||
private TextView reblogs;
|
||||
private TextView favourites;
|
||||
|
@ -44,8 +45,8 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
|||
private TextView cardUrl;
|
||||
private View infoDivider;
|
||||
|
||||
StatusDetailedViewHolder(View view, boolean useAbsoluteTime) {
|
||||
super(view, useAbsoluteTime);
|
||||
StatusDetailedViewHolder(View view) {
|
||||
super(view);
|
||||
reblogs = view.findViewById(R.id.status_reblogs);
|
||||
favourites = view.findViewById(R.id.status_favourites);
|
||||
cardView = view.findViewById(R.id.card_view);
|
||||
|
@ -125,10 +126,11 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void setupWithStatus(final StatusViewData.Concrete status, final StatusActionListener listener,
|
||||
boolean mediaPreviewEnabled, boolean showBotOverlay, boolean animateAvatar,
|
||||
protected void setupWithStatus(final StatusViewData.Concrete status,
|
||||
final StatusActionListener listener,
|
||||
StatusDisplayOptions statusDisplayOptions,
|
||||
@Nullable Object payloads) {
|
||||
super.setupWithStatus(status, listener, mediaPreviewEnabled, showBotOverlay, animateAvatar, payloads);
|
||||
super.setupWithStatus(status, listener, statusDisplayOptions, payloads);
|
||||
if (payloads == null) {
|
||||
setReblogAndFavCount(status.getReblogsCount(), status.getFavouritesCount(), listener);
|
||||
|
||||
|
|
|
@ -22,14 +22,15 @@ import android.view.View;
|
|||
import android.widget.TextView;
|
||||
import android.widget.ToggleButton;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||
import com.keylesspalace.tusky.util.SmartLengthInputFilter;
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions;
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import at.connyduck.sparkbutton.helpers.Utils;
|
||||
|
||||
public class StatusViewHolder extends StatusBaseViewHolder {
|
||||
|
@ -39,8 +40,8 @@ public class StatusViewHolder extends StatusBaseViewHolder {
|
|||
private TextView statusInfo;
|
||||
private ToggleButton contentCollapseButton;
|
||||
|
||||
public StatusViewHolder(View itemView, boolean useAbsoluteTime) {
|
||||
super(itemView, useAbsoluteTime);
|
||||
public StatusViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
statusInfo = itemView.findViewById(R.id.status_info);
|
||||
contentCollapseButton = itemView.findViewById(R.id.button_toggle_content);
|
||||
}
|
||||
|
@ -51,8 +52,9 @@ public class StatusViewHolder extends StatusBaseViewHolder {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener,
|
||||
boolean mediaPreviewEnabled, boolean showBotOverlay, boolean animateAvatar,
|
||||
protected void setupWithStatus(StatusViewData.Concrete status,
|
||||
final StatusActionListener listener,
|
||||
StatusDisplayOptions statusDisplayOptions,
|
||||
@Nullable Object payloads) {
|
||||
if (payloads == null) {
|
||||
|
||||
|
@ -67,7 +69,7 @@ public class StatusViewHolder extends StatusBaseViewHolder {
|
|||
}
|
||||
|
||||
}
|
||||
super.setupWithStatus(status, listener, mediaPreviewEnabled, showBotOverlay, animateAvatar, payloads);
|
||||
super.setupWithStatus(status, listener, statusDisplayOptions, payloads);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -15,15 +15,17 @@
|
|||
|
||||
package com.keylesspalace.tusky.adapter;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions;
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
@ -34,20 +36,14 @@ public class ThreadAdapter extends RecyclerView.Adapter {
|
|||
private static final int VIEW_TYPE_STATUS_DETAILED = 1;
|
||||
|
||||
private List<StatusViewData.Concrete> statuses;
|
||||
private StatusDisplayOptions statusDisplayOptions;
|
||||
private StatusActionListener statusActionListener;
|
||||
private boolean mediaPreviewEnabled;
|
||||
private boolean useAbsoluteTime;
|
||||
private boolean showBotOverlay;
|
||||
private boolean animateAvatar;
|
||||
private int detailedStatusPosition;
|
||||
|
||||
public ThreadAdapter(StatusActionListener listener) {
|
||||
public ThreadAdapter(StatusDisplayOptions statusDisplayOptions, StatusActionListener listener) {
|
||||
this.statusDisplayOptions = statusDisplayOptions;
|
||||
this.statusActionListener = listener;
|
||||
this.statuses = new ArrayList<>();
|
||||
mediaPreviewEnabled = true;
|
||||
useAbsoluteTime = false;
|
||||
showBotOverlay = true;
|
||||
animateAvatar = false;
|
||||
detailedStatusPosition = RecyclerView.NO_POSITION;
|
||||
}
|
||||
|
||||
|
@ -59,12 +55,12 @@ public class ThreadAdapter extends RecyclerView.Adapter {
|
|||
case VIEW_TYPE_STATUS: {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_status, parent, false);
|
||||
return new StatusViewHolder(view, useAbsoluteTime);
|
||||
return new StatusViewHolder(view);
|
||||
}
|
||||
case VIEW_TYPE_STATUS_DETAILED: {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_status_detailed, parent, false);
|
||||
return new StatusDetailedViewHolder(view, useAbsoluteTime);
|
||||
return new StatusDetailedViewHolder(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -74,10 +70,10 @@ public class ThreadAdapter extends RecyclerView.Adapter {
|
|||
StatusViewData.Concrete status = statuses.get(position);
|
||||
if (position == detailedStatusPosition) {
|
||||
StatusDetailedViewHolder holder = (StatusDetailedViewHolder) viewHolder;
|
||||
holder.setupWithStatus(status, statusActionListener, mediaPreviewEnabled, showBotOverlay, animateAvatar);
|
||||
holder.setupWithStatus(status, statusActionListener, statusDisplayOptions);
|
||||
} else {
|
||||
StatusViewHolder holder = (StatusViewHolder) viewHolder;
|
||||
holder.setupWithStatus(status, statusActionListener, mediaPreviewEnabled, showBotOverlay, animateAvatar);
|
||||
holder.setupWithStatus(status, statusActionListener, statusDisplayOptions);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -151,22 +147,6 @@ public class ThreadAdapter extends RecyclerView.Adapter {
|
|||
}
|
||||
}
|
||||
|
||||
public void setMediaPreviewEnabled(boolean enabled) {
|
||||
mediaPreviewEnabled = enabled;
|
||||
}
|
||||
|
||||
public void setUseAbsoluteTime(boolean useAbsoluteTime) {
|
||||
this.useAbsoluteTime = useAbsoluteTime;
|
||||
}
|
||||
|
||||
public void setShowBotOverlay(boolean showBotOverlay) {
|
||||
this.showBotOverlay = showBotOverlay;
|
||||
}
|
||||
|
||||
public void setAnimateAvatar(boolean animateAvatar) {
|
||||
this.animateAvatar = animateAvatar;
|
||||
}
|
||||
|
||||
public void setDetailedStatusPosition(int position) {
|
||||
if (position != detailedStatusPosition
|
||||
&& detailedStatusPosition != RecyclerView.NO_POSITION) {
|
||||
|
|
|
@ -15,19 +15,21 @@
|
|||
|
||||
package com.keylesspalace.tusky.adapter;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import java.util.List;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions;
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public final class TimelineAdapter extends RecyclerView.Adapter {
|
||||
|
||||
public interface AdapterDataSource<T> {
|
||||
|
@ -40,20 +42,29 @@ public final class TimelineAdapter extends RecyclerView.Adapter {
|
|||
private static final int VIEW_TYPE_PLACEHOLDER = 2;
|
||||
|
||||
private final AdapterDataSource<StatusViewData> dataSource;
|
||||
private StatusDisplayOptions statusDisplayOptions;
|
||||
private final StatusActionListener statusListener;
|
||||
private boolean mediaPreviewEnabled;
|
||||
private boolean useAbsoluteTime;
|
||||
private boolean showBotOverlay;
|
||||
private boolean animateAvatar;
|
||||
|
||||
public TimelineAdapter(AdapterDataSource<StatusViewData> dataSource,
|
||||
StatusDisplayOptions statusDisplayOptions,
|
||||
StatusActionListener statusListener) {
|
||||
this.dataSource = dataSource;
|
||||
this.statusDisplayOptions = statusDisplayOptions;
|
||||
this.statusListener = statusListener;
|
||||
mediaPreviewEnabled = true;
|
||||
useAbsoluteTime = false;
|
||||
showBotOverlay = true;
|
||||
animateAvatar = false;
|
||||
}
|
||||
|
||||
public boolean getMediaPreviewEnabled() {
|
||||
return statusDisplayOptions.mediaPreviewEnabled();
|
||||
}
|
||||
|
||||
public void setMediaPreviewEnabled(boolean mediaPreviewEnabled) {
|
||||
this.statusDisplayOptions = statusDisplayOptions.copy(
|
||||
statusDisplayOptions.animateAvatars(),
|
||||
mediaPreviewEnabled,
|
||||
statusDisplayOptions.useAbsoluteTime(),
|
||||
statusDisplayOptions.showBotOverlay(),
|
||||
statusDisplayOptions.useBlurhash()
|
||||
);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
@ -64,7 +75,7 @@ public final class TimelineAdapter extends RecyclerView.Adapter {
|
|||
case VIEW_TYPE_STATUS: {
|
||||
View view = LayoutInflater.from(viewGroup.getContext())
|
||||
.inflate(R.layout.item_status, viewGroup, false);
|
||||
return new StatusViewHolder(view, useAbsoluteTime);
|
||||
return new StatusViewHolder(view);
|
||||
}
|
||||
case VIEW_TYPE_PLACEHOLDER: {
|
||||
View view = LayoutInflater.from(viewGroup.getContext())
|
||||
|
@ -76,16 +87,16 @@ public final class TimelineAdapter extends RecyclerView.Adapter {
|
|||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
|
||||
bindViewHolder(viewHolder,position,null);
|
||||
bindViewHolder(viewHolder, position, null);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position, @NonNull List payloads) {
|
||||
bindViewHolder(viewHolder,position,payloads);
|
||||
bindViewHolder(viewHolder, position, payloads);
|
||||
}
|
||||
|
||||
private void bindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position, @Nullable List payloads){
|
||||
private void bindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position, @Nullable List payloads) {
|
||||
StatusViewData status = dataSource.getItemAt(position);
|
||||
if (status instanceof StatusViewData.Placeholder) {
|
||||
PlaceholderViewHolder holder = (PlaceholderViewHolder) viewHolder;
|
||||
|
@ -94,12 +105,11 @@ public final class TimelineAdapter extends RecyclerView.Adapter {
|
|||
StatusViewHolder holder = (StatusViewHolder) viewHolder;
|
||||
holder.setupWithStatus((StatusViewData.Concrete) status,
|
||||
statusListener,
|
||||
mediaPreviewEnabled,
|
||||
showBotOverlay,
|
||||
animateAvatar,
|
||||
statusDisplayOptions,
|
||||
payloads != null && !payloads.isEmpty() ? payloads.get(0) : null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return dataSource.getItemCount();
|
||||
|
@ -114,26 +124,6 @@ public final class TimelineAdapter extends RecyclerView.Adapter {
|
|||
}
|
||||
}
|
||||
|
||||
public void setMediaPreviewEnabled(boolean enabled) {
|
||||
mediaPreviewEnabled = enabled;
|
||||
}
|
||||
|
||||
public void setUseAbsoluteTime(boolean useAbsoluteTime){
|
||||
this.useAbsoluteTime = useAbsoluteTime;
|
||||
}
|
||||
|
||||
public boolean getMediaPreviewEnabled() {
|
||||
return mediaPreviewEnabled;
|
||||
}
|
||||
|
||||
public void setShowBotOverlay(boolean showBotOverlay) {
|
||||
this.showBotOverlay = showBotOverlay;
|
||||
}
|
||||
|
||||
public void setAnimateAvatar(boolean animateAvatar) {
|
||||
this.animateAvatar = animateAvatar;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return dataSource.getItemAt(position).getViewDataId();
|
||||
|
|
|
@ -12,20 +12,21 @@ import com.keylesspalace.tusky.R
|
|||
import com.keylesspalace.tusky.adapter.NetworkStateViewHolder
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
||||
import com.keylesspalace.tusky.util.NetworkState
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||
|
||||
class ConversationAdapter(private val useAbsoluteTime: Boolean,
|
||||
private val mediaPreviewEnabled: Boolean,
|
||||
class ConversationAdapter(
|
||||
private val statusDisplayOptions: StatusDisplayOptions,
|
||||
private val listener: StatusActionListener,
|
||||
private val topLoadedCallback: () -> Unit,
|
||||
private val retryCallback: () -> Unit)
|
||||
: RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
private val retryCallback: () -> Unit
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
private var networkState: NetworkState? = null
|
||||
|
||||
private val differ: AsyncPagedListDiffer<ConversationEntity> = AsyncPagedListDiffer(object: ListUpdateCallback {
|
||||
private val differ: AsyncPagedListDiffer<ConversationEntity> = AsyncPagedListDiffer(object : ListUpdateCallback {
|
||||
override fun onInserted(position: Int, count: Int) {
|
||||
notifyItemRangeInserted(position, count)
|
||||
if(position == 0) {
|
||||
if (position == 0) {
|
||||
topLoadedCallback()
|
||||
}
|
||||
}
|
||||
|
@ -51,7 +52,8 @@ class ConversationAdapter(private val useAbsoluteTime: Boolean,
|
|||
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
|
||||
return when (viewType) {
|
||||
R.layout.item_network_state -> NetworkStateViewHolder(view, retryCallback)
|
||||
R.layout.item_conversation -> ConversationViewHolder(view, listener, useAbsoluteTime, mediaPreviewEnabled)
|
||||
R.layout.item_conversation -> ConversationViewHolder(view, statusDisplayOptions,
|
||||
listener)
|
||||
else -> throw IllegalArgumentException("unknown view type $viewType")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,6 @@ import android.widget.ImageView;
|
|||
import android.widget.TextView;
|
||||
import android.widget.ToggleButton;
|
||||
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.keylesspalace.tusky.R;
|
||||
|
@ -32,6 +31,7 @@ import com.keylesspalace.tusky.entity.Attachment;
|
|||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||
import com.keylesspalace.tusky.util.ImageLoadingHelper;
|
||||
import com.keylesspalace.tusky.util.SmartLengthInputFilter;
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions;
|
||||
import com.keylesspalace.tusky.viewdata.PollViewDataKt;
|
||||
|
||||
import java.util.List;
|
||||
|
@ -44,15 +44,13 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
|
|||
private ToggleButton contentCollapseButton;
|
||||
private ImageView[] avatars;
|
||||
|
||||
private StatusDisplayOptions statusDisplayOptions;
|
||||
private StatusActionListener listener;
|
||||
private boolean mediaPreviewEnabled;
|
||||
private boolean animateAvatars;
|
||||
|
||||
ConversationViewHolder(View itemView,
|
||||
StatusActionListener listener,
|
||||
boolean useAbsoluteTime,
|
||||
boolean mediaPreviewEnabled) {
|
||||
super(itemView, useAbsoluteTime);
|
||||
StatusDisplayOptions statusDisplayOptions,
|
||||
StatusActionListener listener) {
|
||||
super(itemView);
|
||||
conversationNameTextView = itemView.findViewById(R.id.conversation_name);
|
||||
contentCollapseButton = itemView.findViewById(R.id.button_toggle_content);
|
||||
avatars = new ImageView[]{
|
||||
|
@ -60,11 +58,10 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
|
|||
itemView.findViewById(R.id.status_avatar_1),
|
||||
itemView.findViewById(R.id.status_avatar_2)
|
||||
};
|
||||
this.statusDisplayOptions = statusDisplayOptions;
|
||||
|
||||
this.listener = listener;
|
||||
this.mediaPreviewEnabled = mediaPreviewEnabled;
|
||||
|
||||
this.animateAvatars = PreferenceManager.getDefaultSharedPreferences(itemView.getContext()).getBoolean("animateGifAvatars", false);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -86,8 +83,9 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
|
|||
setBookmarked(status.getBookmarked());
|
||||
List<Attachment> attachments = status.getAttachments();
|
||||
boolean sensitive = status.getSensitive();
|
||||
if(mediaPreviewEnabled && !hasAudioAttachment(attachments)) {
|
||||
setMediaPreviews(attachments, sensitive, listener, status.getShowingHiddenContent());
|
||||
if (statusDisplayOptions.mediaPreviewEnabled() && !hasAudioAttachment(attachments)) {
|
||||
setMediaPreviews(attachments, sensitive, listener, status.getShowingHiddenContent(),
|
||||
statusDisplayOptions.useBlurhash());
|
||||
|
||||
if (attachments.size() == 0) {
|
||||
hideSensitiveMediaWarning();
|
||||
|
@ -118,11 +116,11 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
|
|||
private void setConversationName(List<ConversationAccountEntity> accounts) {
|
||||
Context context = conversationNameTextView.getContext();
|
||||
String conversationName = "";
|
||||
if(accounts.size() == 1) {
|
||||
if (accounts.size() == 1) {
|
||||
conversationName = context.getString(R.string.conversation_1_recipients, accounts.get(0).getUsername());
|
||||
} else if(accounts.size() == 2) {
|
||||
} else if (accounts.size() == 2) {
|
||||
conversationName = context.getString(R.string.conversation_2_recipients, accounts.get(0).getUsername(), accounts.get(1).getUsername());
|
||||
} else if (accounts.size() > 2){
|
||||
} else if (accounts.size() > 2) {
|
||||
conversationName = context.getString(R.string.conversation_more_recipients, accounts.get(0).getUsername(), accounts.get(1).getUsername(), accounts.size() - 2);
|
||||
}
|
||||
|
||||
|
@ -130,10 +128,11 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
|
|||
}
|
||||
|
||||
private void setAvatars(List<ConversationAccountEntity> accounts) {
|
||||
for(int i=0; i < avatars.length; i++) {
|
||||
for (int i = 0; i < avatars.length; i++) {
|
||||
ImageView avatarView = avatars[i];
|
||||
if(i < accounts.size()) {
|
||||
ImageLoadingHelper.loadAvatar(accounts.get(i).getAvatar(), avatarView, avatarRadius48dp, animateAvatars);
|
||||
if (i < accounts.size()) {
|
||||
ImageLoadingHelper.loadAvatar(accounts.get(i).getAvatar(), avatarView,
|
||||
avatarRadius48dp, statusDisplayOptions.animateAvatars());
|
||||
avatarView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
avatarView.setVisibility(View.GONE);
|
||||
|
|
|
@ -37,6 +37,7 @@ import com.keylesspalace.tusky.fragment.SFragment
|
|||
import com.keylesspalace.tusky.interfaces.ReselectableFragment
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
||||
import com.keylesspalace.tusky.util.NetworkState
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||
import com.keylesspalace.tusky.util.ThemeUtils
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import kotlinx.android.synthetic.main.fragment_timeline.*
|
||||
|
@ -62,15 +63,18 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
|||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(view.context)
|
||||
val useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false)
|
||||
|
||||
val account = accountManager.activeAccount
|
||||
val mediaPreviewEnabled = account?.mediaPreviewEnabled ?: true
|
||||
val statusDisplayOptions = StatusDisplayOptions(
|
||||
animateAvatars = preferences.getBoolean("animateGifAvatars", false),
|
||||
mediaPreviewEnabled = accountManager.activeAccount?.mediaPreviewEnabled ?: true,
|
||||
useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false),
|
||||
showBotOverlay = preferences.getBoolean("showBotOverlay", true),
|
||||
useBlurhash = preferences.getBoolean("useBlurhash", true)
|
||||
)
|
||||
|
||||
|
||||
adapter = ConversationAdapter(useAbsoluteTime, mediaPreviewEnabled, this, ::onTopLoaded, viewModel::retry)
|
||||
adapter = ConversationAdapter(statusDisplayOptions, this, ::onTopLoaded, viewModel::retry)
|
||||
|
||||
recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
|
||||
layoutManager = LinearLayoutManager(view.context)
|
||||
|
|
|
@ -31,12 +31,13 @@ import com.keylesspalace.tusky.viewdata.toViewData
|
|||
import kotlinx.android.synthetic.main.item_report_status.view.*
|
||||
import java.util.*
|
||||
|
||||
class StatusViewHolder(itemView: View,
|
||||
private val useAbsoluteTime: Boolean,
|
||||
private val mediaPreviewEnabled: Boolean,
|
||||
class StatusViewHolder(
|
||||
itemView: View,
|
||||
private val statusDisplayOptions: StatusDisplayOptions,
|
||||
private val viewState: StatusViewState,
|
||||
private val adapterHandler: AdapterHandler,
|
||||
private val getStatusForPosition: (Int) -> Status?) : RecyclerView.ViewHolder(itemView) {
|
||||
private val getStatusForPosition: (Int) -> Status?
|
||||
) : RecyclerView.ViewHolder(itemView) {
|
||||
private val mediaViewHeight = itemView.context.resources.getDimensionPixelSize(R.dimen.status_media_preview_height)
|
||||
private val statusViewHelper = StatusViewHelper(itemView)
|
||||
|
||||
|
@ -69,11 +70,11 @@ class StatusViewHolder(itemView: View,
|
|||
|
||||
val sensitive = status.sensitive
|
||||
|
||||
statusViewHelper.setMediasPreview(mediaPreviewEnabled, status.attachments, sensitive, previewListener,
|
||||
viewState.isMediaShow(status.id, status.sensitive),
|
||||
statusViewHelper.setMediasPreview(statusDisplayOptions, status.attachments,
|
||||
sensitive, previewListener, viewState.isMediaShow(status.id, status.sensitive),
|
||||
mediaViewHeight)
|
||||
|
||||
statusViewHelper.setupPollReadonly(status.poll.toViewData(), status.emojis, useAbsoluteTime)
|
||||
statusViewHelper.setupPollReadonly(status.poll.toViewData(), status.emojis, statusDisplayOptions.useAbsoluteTime)
|
||||
setCreatedAt(status.createdAt)
|
||||
}
|
||||
|
||||
|
@ -124,7 +125,7 @@ class StatusViewHolder(itemView: View,
|
|||
}
|
||||
|
||||
private fun setCreatedAt(createdAt: Date?) {
|
||||
if (useAbsoluteTime) {
|
||||
if (statusDisplayOptions.useAbsoluteTime) {
|
||||
itemView.timestampInfo.text = statusViewHelper.getAbsoluteTime(createdAt)
|
||||
} else {
|
||||
itemView.timestampInfo.text = if (createdAt != null) {
|
||||
|
|
|
@ -23,12 +23,13 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.components.report.model.StatusViewState
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||
|
||||
class StatusesAdapter(private val useAbsoluteTime: Boolean,
|
||||
private val mediaPreviewEnabled: Boolean,
|
||||
class StatusesAdapter(
|
||||
private val statusDisplayOptions: StatusDisplayOptions,
|
||||
private val statusViewState: StatusViewState,
|
||||
private val adapterHandler: AdapterHandler)
|
||||
: PagedListAdapter<Status, RecyclerView.ViewHolder>(STATUS_COMPARATOR) {
|
||||
private val adapterHandler: AdapterHandler
|
||||
) : PagedListAdapter<Status, RecyclerView.ViewHolder>(STATUS_COMPARATOR) {
|
||||
|
||||
private val statusForPosition: (Int) -> Status? = { position: Int ->
|
||||
if (position != RecyclerView.NO_POSITION) getItem(position) else null
|
||||
|
@ -36,8 +37,10 @@ class StatusesAdapter(private val useAbsoluteTime: Boolean,
|
|||
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return StatusViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_report_status, parent, false),
|
||||
useAbsoluteTime, mediaPreviewEnabled, statusViewState, adapterHandler, statusForPosition)
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_report_status, parent, false)
|
||||
return StatusViewHolder(view, statusDisplayOptions, statusViewState, adapterHandler,
|
||||
statusForPosition)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
|
|
|
@ -43,6 +43,7 @@ import com.keylesspalace.tusky.di.Injectable
|
|||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
import com.keylesspalace.tusky.entity.Attachment
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||
import com.keylesspalace.tusky.util.ThemeUtils
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.show
|
||||
|
@ -119,14 +120,16 @@ class ReportStatusesFragment : Fragment(), Injectable, AdapterHandler {
|
|||
|
||||
private fun initStatusesView() {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
val statusDisplayOptions = StatusDisplayOptions(
|
||||
animateAvatars = false,
|
||||
mediaPreviewEnabled = accountManager.activeAccount?.mediaPreviewEnabled ?: true,
|
||||
useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false),
|
||||
showBotOverlay = false,
|
||||
useBlurhash = preferences.getBoolean("useBlurhash", true)
|
||||
)
|
||||
|
||||
val useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false)
|
||||
|
||||
val account = accountManager.activeAccount
|
||||
val mediaPreviewEnabled = account?.mediaPreviewEnabled ?: true
|
||||
|
||||
|
||||
adapter = StatusesAdapter(useAbsoluteTime, mediaPreviewEnabled, viewModel.statusViewState, this)
|
||||
adapter = StatusesAdapter(statusDisplayOptions,
|
||||
viewModel.statusViewState, this)
|
||||
|
||||
recyclerView.addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL))
|
||||
layoutManager = LinearLayoutManager(requireContext())
|
||||
|
|
|
@ -24,28 +24,26 @@ import com.keylesspalace.tusky.R
|
|||
import com.keylesspalace.tusky.adapter.StatusViewHolder
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
|
||||
class SearchStatusesAdapter(private val useAbsoluteTime: Boolean,
|
||||
private val mediaPreviewEnabled: Boolean,
|
||||
private val showBotOverlay: Boolean,
|
||||
private val animateAvatar: Boolean,
|
||||
private val statusListener: StatusActionListener)
|
||||
: PagedListAdapter<Pair<Status, StatusViewData.Concrete>, RecyclerView.ViewHolder>(STATUS_COMPARATOR) {
|
||||
class SearchStatusesAdapter(
|
||||
private val statusDisplayOptions: StatusDisplayOptions,
|
||||
private val statusListener: StatusActionListener
|
||||
) : PagedListAdapter<Pair<Status, StatusViewData.Concrete>, RecyclerView.ViewHolder>(STATUS_COMPARATOR) {
|
||||
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_status, parent, false)
|
||||
return StatusViewHolder(view, useAbsoluteTime)
|
||||
return StatusViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
getItem(position)?.let { item ->
|
||||
(holder as? StatusViewHolder)?.setupWithStatus(item.second, statusListener,
|
||||
mediaPreviewEnabled, showBotOverlay, animateAvatar)
|
||||
statusDisplayOptions)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public override fun getItem(position: Int): Pair<Status, StatusViewData.Concrete>? {
|
||||
|
|
|
@ -52,6 +52,7 @@ import com.keylesspalace.tusky.entity.Status
|
|||
import com.keylesspalace.tusky.interfaces.AccountSelectionListener
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
||||
import com.keylesspalace.tusky.util.NetworkState
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||
import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
|
||||
|
@ -71,13 +72,17 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
|
|||
|
||||
override fun createAdapter(): PagedListAdapter<Pair<Status, StatusViewData.Concrete>, *> {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(searchRecyclerView.context)
|
||||
val useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false)
|
||||
val showBotOverlay = preferences.getBoolean("showBotOverlay", true)
|
||||
val animateAvatar = preferences.getBoolean("animateGifAvatars", false)
|
||||
val statusDisplayOptions = StatusDisplayOptions(
|
||||
animateAvatars = preferences.getBoolean("animateGifAvatars", false),
|
||||
mediaPreviewEnabled = viewModel.mediaPreviewEnabled,
|
||||
useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false),
|
||||
showBotOverlay = preferences.getBoolean("showBotOverlay", true),
|
||||
useBlurhash = preferences.getBoolean("useBlurhash", true)
|
||||
)
|
||||
|
||||
searchRecyclerView.addItemDecoration(DividerItemDecoration(searchRecyclerView.context, DividerItemDecoration.VERTICAL))
|
||||
searchRecyclerView.layoutManager = LinearLayoutManager(searchRecyclerView.context)
|
||||
return SearchStatusesAdapter(useAbsoluteTime, viewModel.mediaPreviewEnabled, showBotOverlay, animateAvatar, this)
|
||||
return SearchStatusesAdapter(statusDisplayOptions, this)
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -31,7 +31,8 @@ data class Attachment(
|
|||
@SerializedName("preview_url") val previewUrl: String,
|
||||
val meta: MetaData?,
|
||||
val type: Type,
|
||||
val description: String?
|
||||
val description: String?,
|
||||
val blurhash: String?
|
||||
) : Parcelable {
|
||||
|
||||
@JsonAdapter(MediaTypeDeserializer::class)
|
||||
|
|
|
@ -75,6 +75,7 @@ import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate;
|
|||
import com.keylesspalace.tusky.util.ListUtils;
|
||||
import com.keylesspalace.tusky.util.NotificationTypeConverterKt;
|
||||
import com.keylesspalace.tusky.util.PairedList;
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions;
|
||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
||||
import com.keylesspalace.tusky.util.ViewDataUtils;
|
||||
import com.keylesspalace.tusky.view.BackgroundMessageView;
|
||||
|
@ -237,18 +238,18 @@ public class NotificationsFragment extends SFragment implements
|
|||
|
||||
recyclerView.addItemDecoration(new DividerItemDecoration(context, DividerItemDecoration.VERTICAL));
|
||||
|
||||
StatusDisplayOptions statusDisplayOptions = new StatusDisplayOptions(
|
||||
preferences.getBoolean("animateGifAvatars", false),
|
||||
accountManager.getActiveAccount().getMediaPreviewEnabled(),
|
||||
preferences.getBoolean("absoluteTimeView", false),
|
||||
preferences.getBoolean("showBotOverlay", true),
|
||||
preferences.getBoolean("useBlurhash", true)
|
||||
);
|
||||
|
||||
adapter = new NotificationsAdapter(accountManager.getActiveAccount().getAccountId(),
|
||||
dataSource, this, this);
|
||||
dataSource, statusDisplayOptions, this, this);
|
||||
alwaysShowSensitiveMedia = accountManager.getActiveAccount().getAlwaysShowSensitiveMedia();
|
||||
alwaysOpenSpoiler = accountManager.getActiveAccount().getAlwaysOpenSpoiler();
|
||||
boolean mediaPreviewEnabled = accountManager.getActiveAccount().getMediaPreviewEnabled();
|
||||
adapter.setMediaPreviewEnabled(mediaPreviewEnabled);
|
||||
boolean useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false);
|
||||
adapter.setUseAbsoluteTime(useAbsoluteTime);
|
||||
boolean showBotOverlay = preferences.getBoolean("showBotOverlay", true);
|
||||
adapter.setShowBotOverlay(showBotOverlay);
|
||||
boolean animateAvatar = preferences.getBoolean("animateGifAvatars", false);
|
||||
adapter.setAnimateAvatar(animateAvatar);
|
||||
recyclerView.setAdapter(adapter);
|
||||
|
||||
topLoading = false;
|
||||
|
|
|
@ -77,6 +77,7 @@ import com.keylesspalace.tusky.util.LinkHelper;
|
|||
import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate;
|
||||
import com.keylesspalace.tusky.util.ListUtils;
|
||||
import com.keylesspalace.tusky.util.PairedList;
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions;
|
||||
import com.keylesspalace.tusky.util.StringUtils;
|
||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
||||
import com.keylesspalace.tusky.util.ViewDataUtils;
|
||||
|
@ -219,7 +220,15 @@ public class TimelineFragment extends SFragment implements
|
|||
hashtagOrId = arguments.getString(HASHTAG_OR_ID_ARG);
|
||||
}
|
||||
|
||||
adapter = new TimelineAdapter(dataSource, this);
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
|
||||
StatusDisplayOptions statusDisplayOptions = new StatusDisplayOptions(
|
||||
preferences.getBoolean("animateGifAvatars", false),
|
||||
accountManager.getActiveAccount().getMediaPreviewEnabled(),
|
||||
preferences.getBoolean("absoluteTimeView", false),
|
||||
preferences.getBoolean("showBotOverlay", true),
|
||||
preferences.getBoolean("useBlurhash", true)
|
||||
);
|
||||
adapter = new TimelineAdapter(dataSource, statusDisplayOptions, this);
|
||||
|
||||
isSwipeToRefreshEnabled = arguments.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true);
|
||||
|
||||
|
@ -341,18 +350,10 @@ public class TimelineFragment extends SFragment implements
|
|||
}
|
||||
|
||||
private void setupTimelinePreferences() {
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
|
||||
alwaysShowSensitiveMedia = accountManager.getActiveAccount().getAlwaysShowSensitiveMedia();
|
||||
alwaysOpenSpoiler = accountManager.getActiveAccount().getAlwaysOpenSpoiler();
|
||||
boolean mediaPreviewEnabled = accountManager.getActiveAccount().getMediaPreviewEnabled();
|
||||
adapter.setMediaPreviewEnabled(mediaPreviewEnabled);
|
||||
boolean useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false);
|
||||
adapter.setUseAbsoluteTime(useAbsoluteTime);
|
||||
boolean showBotOverlay = preferences.getBoolean("showBotOverlay", true);
|
||||
adapter.setShowBotOverlay(showBotOverlay);
|
||||
boolean animateAvatar = preferences.getBoolean("animateGifAvatars", false);
|
||||
adapter.setAnimateAvatar(animateAvatar);
|
||||
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
boolean filter = preferences.getBoolean("tabFilterHomeReplies", true);
|
||||
filterRemoveReplies = kind == Kind.HOME && !filter;
|
||||
|
||||
|
|
|
@ -60,6 +60,7 @@ import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
|||
import com.keylesspalace.tusky.network.MastodonApi;
|
||||
import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate;
|
||||
import com.keylesspalace.tusky.util.PairedList;
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions;
|
||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
||||
import com.keylesspalace.tusky.util.ViewDataUtils;
|
||||
import com.keylesspalace.tusky.view.ConversationLineItemDecoration;
|
||||
|
@ -123,8 +124,16 @@ public final class ViewThreadFragment extends SFragment implements
|
|||
super.onCreate(savedInstanceState);
|
||||
|
||||
thisThreadsStatusId = getArguments().getString("id");
|
||||
|
||||
adapter = new ThreadAdapter(this);
|
||||
SharedPreferences preferences =
|
||||
PreferenceManager.getDefaultSharedPreferences(getActivity());
|
||||
StatusDisplayOptions statusDisplayOptions = new StatusDisplayOptions(
|
||||
preferences.getBoolean("animateGifAvatars", false),
|
||||
accountManager.getActiveAccount().getMediaPreviewEnabled(),
|
||||
preferences.getBoolean("absoluteTimeView", false),
|
||||
preferences.getBoolean("showBotOverlay", true),
|
||||
preferences.getBoolean("useBlurhash", true)
|
||||
);
|
||||
adapter = new ThreadAdapter(statusDisplayOptions, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -150,18 +159,8 @@ public final class ViewThreadFragment extends SFragment implements
|
|||
recyclerView.addItemDecoration(divider);
|
||||
|
||||
recyclerView.addItemDecoration(new ConversationLineItemDecoration(context));
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(
|
||||
getActivity());
|
||||
alwaysShowSensitiveMedia = accountManager.getActiveAccount().getAlwaysShowSensitiveMedia();
|
||||
alwaysOpenSpoiler = accountManager.getActiveAccount().getAlwaysOpenSpoiler();
|
||||
boolean mediaPreviewEnabled = accountManager.getActiveAccount().getMediaPreviewEnabled();
|
||||
adapter.setMediaPreviewEnabled(mediaPreviewEnabled);
|
||||
boolean useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false);
|
||||
adapter.setUseAbsoluteTime(useAbsoluteTime);
|
||||
boolean animateAvatars = preferences.getBoolean("animateGifAvatars", false);
|
||||
adapter.setAnimateAvatar(animateAvatars);
|
||||
boolean showBotIndicator = preferences.getBoolean("showBotOverlay", true);
|
||||
adapter.setShowBotOverlay(showBotIndicator);
|
||||
reloadFilters(false);
|
||||
|
||||
recyclerView.setAdapter(adapter);
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
/**
|
||||
* Blurhash implementation from blurhash project:
|
||||
* https://github.com/woltapp/blurhash
|
||||
* Minor modifications by charlag
|
||||
*/
|
||||
|
||||
package com.keylesspalace.tusky.util
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.withSign
|
||||
|
||||
object BlurHashDecoder {
|
||||
|
||||
fun decode(blurHash: String?, width: Int, height: Int, punch: Float = 1f): Bitmap? {
|
||||
require(width > 0) { "Width must be greater than zero" }
|
||||
require(height > 0) { "height must be greater than zero" }
|
||||
if (blurHash == null || blurHash.length < 6) {
|
||||
return null
|
||||
}
|
||||
val numCompEnc = decode83(blurHash, 0, 1)
|
||||
val numCompX = (numCompEnc % 9) + 1
|
||||
val numCompY = (numCompEnc / 9) + 1
|
||||
if (blurHash.length != 4 + 2 * numCompX * numCompY) {
|
||||
return null
|
||||
}
|
||||
val maxAcEnc = decode83(blurHash, 1, 2)
|
||||
val maxAc = (maxAcEnc + 1) / 166f
|
||||
val colors = Array(numCompX * numCompY) { i ->
|
||||
if (i == 0) {
|
||||
val colorEnc = decode83(blurHash, 2, 6)
|
||||
decodeDc(colorEnc)
|
||||
} else {
|
||||
val from = 4 + i * 2
|
||||
val colorEnc = decode83(blurHash, from, from + 2)
|
||||
decodeAc(colorEnc, maxAc * punch)
|
||||
}
|
||||
}
|
||||
return composeBitmap(width, height, numCompX, numCompY, colors)
|
||||
}
|
||||
|
||||
private fun decode83(str: String, from: Int = 0, to: Int = str.length): Int {
|
||||
var result = 0
|
||||
for (i in from until to) {
|
||||
val index = charMap[str[i]] ?: -1
|
||||
if (index != -1) {
|
||||
result = result * 83 + index
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun decodeDc(colorEnc: Int): FloatArray {
|
||||
val r = colorEnc shr 16
|
||||
val g = (colorEnc shr 8) and 255
|
||||
val b = colorEnc and 255
|
||||
return floatArrayOf(srgbToLinear(r), srgbToLinear(g), srgbToLinear(b))
|
||||
}
|
||||
|
||||
private fun srgbToLinear(colorEnc: Int): Float {
|
||||
val v = colorEnc / 255f
|
||||
return if (v <= 0.04045f) {
|
||||
(v / 12.92f)
|
||||
} else {
|
||||
((v + 0.055f) / 1.055f).pow(2.4f)
|
||||
}
|
||||
}
|
||||
|
||||
private fun decodeAc(value: Int, maxAc: Float): FloatArray {
|
||||
val r = value / (19 * 19)
|
||||
val g = (value / 19) % 19
|
||||
val b = value % 19
|
||||
return floatArrayOf(
|
||||
signedPow2((r - 9) / 9.0f) * maxAc,
|
||||
signedPow2((g - 9) / 9.0f) * maxAc,
|
||||
signedPow2((b - 9) / 9.0f) * maxAc
|
||||
)
|
||||
}
|
||||
|
||||
private fun signedPow2(value: Float) = value.pow(2f).withSign(value)
|
||||
|
||||
private fun composeBitmap(
|
||||
width: Int, height: Int,
|
||||
numCompX: Int, numCompY: Int,
|
||||
colors: Array<FloatArray>
|
||||
): Bitmap {
|
||||
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
for (y in 0 until height) {
|
||||
for (x in 0 until width) {
|
||||
var r = 0f
|
||||
var g = 0f
|
||||
var b = 0f
|
||||
for (j in 0 until numCompY) {
|
||||
for (i in 0 until numCompX) {
|
||||
val basis = (cos(PI * x * i / width) * cos(PI * y * j / height)).toFloat()
|
||||
val color = colors[j * numCompX + i]
|
||||
r += color[0] * basis
|
||||
g += color[1] * basis
|
||||
b += color[2] * basis
|
||||
}
|
||||
}
|
||||
bitmap.setPixel(x, y, Color.rgb(linearToSrgb(r), linearToSrgb(g), linearToSrgb(b)))
|
||||
}
|
||||
}
|
||||
return bitmap
|
||||
}
|
||||
|
||||
private fun linearToSrgb(value: Float): Int {
|
||||
val v = value.coerceIn(0f, 1f)
|
||||
return if (v <= 0.0031308f) {
|
||||
(v * 12.92f * 255f + 0.5f).toInt()
|
||||
} else {
|
||||
((1.055f * v.pow(1 / 2.4f) - 0.055f) * 255 + 0.5f).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
private val charMap = listOf(
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G',
|
||||
'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
|
||||
'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
|
||||
'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '#', '$', '%', '*', '+', ',',
|
||||
'-', '.', ':', ';', '=', '?', '@', '[', ']', '^', '_', '{', '|', '}', '~'
|
||||
)
|
||||
.mapIndexed { i, c -> c to i }
|
||||
.toMap()
|
||||
|
||||
}
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
package com.keylesspalace.tusky.util
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.widget.ImageView
|
||||
import androidx.annotation.Px
|
||||
import com.bumptech.glide.Glide
|
||||
|
@ -14,7 +16,7 @@ private val centerCropTransformation = CenterCrop()
|
|||
|
||||
fun loadAvatar(url: String?, imageView: ImageView, @Px radius: Int, animate: Boolean) {
|
||||
|
||||
if(url.isNullOrBlank()) {
|
||||
if (url.isNullOrBlank()) {
|
||||
Glide.with(imageView)
|
||||
.load(R.drawable.avatar_default)
|
||||
.into(imageView)
|
||||
|
@ -43,3 +45,7 @@ fun loadAvatar(url: String?, imageView: ImageView, @Px radius: Int, animate: Boo
|
|||
|
||||
}
|
||||
}
|
||||
|
||||
fun decodeBlurHash(context: Context, blurhash: String): BitmapDrawable {
|
||||
return BitmapDrawable(context.resources, BlurHashDecoder.decode(blurhash, 32, 32, 1f))
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package com.keylesspalace.tusky.util
|
||||
|
||||
data class StatusDisplayOptions(
|
||||
@get:JvmName("animateAvatars")
|
||||
val animateAvatars: Boolean,
|
||||
@get:JvmName("mediaPreviewEnabled")
|
||||
val mediaPreviewEnabled: Boolean,
|
||||
@get:JvmName("useAbsoluteTime")
|
||||
val useAbsoluteTime: Boolean,
|
||||
@get:JvmName("showBotOverlay")
|
||||
val showBotOverlay: Boolean,
|
||||
@get:JvmName("useBlurhash")
|
||||
val useBlurhash: Boolean
|
||||
)
|
|
@ -16,6 +16,7 @@
|
|||
package com.keylesspalace.tusky.util
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.text.InputFilter
|
||||
import android.text.TextUtils
|
||||
import android.view.View
|
||||
|
@ -47,7 +48,7 @@ class StatusViewHelper(private val itemView: View) {
|
|||
private val longSdf = SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault())
|
||||
|
||||
fun setMediasPreview(
|
||||
mediaPreviewEnabled: Boolean,
|
||||
statusDisplayOptions: StatusDisplayOptions,
|
||||
attachments: List<Attachment>,
|
||||
sensitive: Boolean,
|
||||
previewListener: MediaPreviewListener,
|
||||
|
@ -70,7 +71,7 @@ class StatusViewHelper(private val itemView: View) {
|
|||
val sensitiveMediaWarning = itemView.findViewById<TextView>(R.id.status_sensitive_media_warning)
|
||||
val sensitiveMediaShow = itemView.findViewById<View>(R.id.status_sensitive_media_button)
|
||||
val mediaLabel = itemView.findViewById<TextView>(R.id.status_media_label)
|
||||
if (mediaPreviewEnabled) {
|
||||
if (statusDisplayOptions.mediaPreviewEnabled) {
|
||||
// Hide the unused label.
|
||||
mediaLabel.visibility = View.GONE
|
||||
} else {
|
||||
|
@ -86,13 +87,15 @@ class StatusViewHelper(private val itemView: View) {
|
|||
}
|
||||
|
||||
|
||||
val mediaPreviewUnloadedId = ThemeUtils.getDrawableId(context, R.attr.media_preview_unloaded_drawable, android.R.color.black)
|
||||
val mediaPreviewUnloaded = ThemeUtils.getDrawable(context,
|
||||
R.attr.media_preview_unloaded_drawable, android.R.color.black)
|
||||
|
||||
val n = min(attachments.size, Status.MAX_MEDIA_ATTACHMENTS)
|
||||
|
||||
for (i in 0 until n) {
|
||||
val previewUrl = attachments[i].previewUrl
|
||||
val description = attachments[i].description
|
||||
val attachment = attachments[i]
|
||||
val previewUrl = attachment.previewUrl
|
||||
val description = attachment.description
|
||||
|
||||
if (TextUtils.isEmpty(description)) {
|
||||
mediaPreviews[i].contentDescription = context.getString(R.string.action_view_media)
|
||||
|
@ -104,19 +107,22 @@ class StatusViewHelper(private val itemView: View) {
|
|||
|
||||
if (TextUtils.isEmpty(previewUrl)) {
|
||||
Glide.with(mediaPreviews[i])
|
||||
.load(mediaPreviewUnloadedId)
|
||||
.load(mediaPreviewUnloaded)
|
||||
.centerInside()
|
||||
.into(mediaPreviews[i])
|
||||
} else {
|
||||
val meta = attachments[i].meta
|
||||
val placeholder = if (attachment.blurhash != null)
|
||||
decodeBlurHash(context, attachment.blurhash)
|
||||
else mediaPreviewUnloaded
|
||||
val meta = attachment.meta
|
||||
val focus = meta?.focus
|
||||
|
||||
if (showingContent) {
|
||||
if (focus != null) { // If there is a focal point for this attachment:
|
||||
mediaPreviews[i].setFocalPoint(focus)
|
||||
|
||||
Glide.with(mediaPreviews[i])
|
||||
.load(previewUrl)
|
||||
.placeholder(mediaPreviewUnloadedId)
|
||||
.placeholder(placeholder)
|
||||
.centerInside()
|
||||
.addListener(mediaPreviews[i])
|
||||
.into(mediaPreviews[i])
|
||||
|
@ -125,14 +131,25 @@ class StatusViewHelper(private val itemView: View) {
|
|||
|
||||
Glide.with(mediaPreviews[i])
|
||||
.load(previewUrl)
|
||||
.placeholder(mediaPreviewUnloadedId)
|
||||
.placeholder(placeholder)
|
||||
.centerInside()
|
||||
.into(mediaPreviews[i])
|
||||
}
|
||||
} else {
|
||||
mediaPreviews[i].removeFocalPoint()
|
||||
if (statusDisplayOptions.useBlurhash && attachment.blurhash != null) {
|
||||
val blurhashBitmap = decodeBlurHash(context, attachment.blurhash)
|
||||
mediaPreviews[i].setImageDrawable(blurhashBitmap)
|
||||
} else {
|
||||
mediaPreviews[i].setImageDrawable(ColorDrawable(ThemeUtils.getColor(
|
||||
context, R.attr.sensitive_media_warning_background_color)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val type = attachments[i].type
|
||||
if ((type === Attachment.Type.VIDEO) or (type === Attachment.Type.GIFV)) {
|
||||
val type = attachment.type
|
||||
if (showingContent
|
||||
&& (type === Attachment.Type.VIDEO) or (type === Attachment.Type.GIFV)) {
|
||||
mediaOverlays[i].visibility = View.VISIBLE
|
||||
} else {
|
||||
mediaOverlays[i].visibility = View.GONE
|
||||
|
@ -158,13 +175,9 @@ class StatusViewHelper(private val itemView: View) {
|
|||
} else {
|
||||
|
||||
val hiddenContentText: String = if (sensitive) {
|
||||
context.getString(R.string.status_sensitive_media_template,
|
||||
context.getString(R.string.status_sensitive_media_title),
|
||||
context.getString(R.string.status_sensitive_media_directions))
|
||||
context.getString(R.string.status_sensitive_media_title)
|
||||
} else {
|
||||
context.getString(R.string.status_sensitive_media_template,
|
||||
context.getString(R.string.status_media_hidden_title),
|
||||
context.getString(R.string.status_sensitive_media_directions))
|
||||
context.getString(R.string.status_media_hidden_title)
|
||||
}
|
||||
|
||||
sensitiveMediaWarning.text = HtmlUtils.fromHtml(hiddenContentText)
|
||||
|
@ -175,11 +188,15 @@ class StatusViewHelper(private val itemView: View) {
|
|||
previewListener.onContentHiddenChange(false)
|
||||
v.visibility = View.GONE
|
||||
sensitiveMediaWarning.visibility = View.VISIBLE
|
||||
setMediasPreview(statusDisplayOptions, attachments, sensitive, previewListener,
|
||||
false, mediaPreviewHeight)
|
||||
}
|
||||
sensitiveMediaWarning.setOnClickListener { v ->
|
||||
previewListener.onContentHiddenChange(true)
|
||||
v.visibility = View.GONE
|
||||
sensitiveMediaShow.visibility = View.VISIBLE
|
||||
setMediasPreview(statusDisplayOptions, attachments, sensitive, previewListener,
|
||||
true, mediaPreviewHeight)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -50,7 +50,7 @@ defStyleAttr: Int = 0
|
|||
/**
|
||||
* Set the focal point for this view.
|
||||
*/
|
||||
fun setFocalPoint(focus: Attachment.Focus) {
|
||||
fun setFocalPoint(focus: Attachment.Focus?) {
|
||||
this.focus = focus
|
||||
super.setScaleType(ScaleType.MATRIX)
|
||||
|
||||
|
|
5
app/src/main/res/drawable/media_warning_bg.xml
Normal file
5
app/src/main/res/drawable/media_warning_bg.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<corners android:radius="8dp" />
|
||||
<solid android:color="?attr/sensitive_media_warning_background_color" />
|
||||
</shape>
|
|
@ -93,7 +93,7 @@
|
|||
app:layout_constraintEnd_toStartOf="@id/barrierEnd"
|
||||
app:layout_constraintStart_toStartOf="@id/guideBegin"
|
||||
app:layout_constraintTop_toBottomOf="@id/buttonToggleContent"
|
||||
tools:visibility="gone">
|
||||
tools:visibility="visible">
|
||||
|
||||
<com.keylesspalace.tusky.view.MediaPreviewImageView
|
||||
android:id="@+id/status_media_preview_0"
|
||||
|
@ -199,17 +199,21 @@
|
|||
android:visibility="gone"
|
||||
app:layout_constraintLeft_toLeftOf="@+id/status_media_preview_container"
|
||||
app:layout_constraintTop_toTopOf="@+id/status_media_preview_container"
|
||||
app:srcCompat="@drawable/ic_eye_24dp" />
|
||||
app:srcCompat="@drawable/ic_eye_24dp"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/status_sensitive_media_warning"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="?attr/sensitive_media_warning_background_color"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/media_warning_bg"
|
||||
android:gravity="center"
|
||||
android:lineSpacingMultiplier="1.2"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp"
|
||||
android:paddingLeft="12dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingRight="12dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:textAlignment="center"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
|
|
|
@ -188,7 +188,7 @@
|
|||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@id/status_display_name"
|
||||
app:layout_constraintTop_toBottomOf="@id/button_toggle_content"
|
||||
tools:visibility="gone">
|
||||
tools:visibility="visible">
|
||||
|
||||
<com.keylesspalace.tusky.view.MediaPreviewImageView
|
||||
android:id="@+id/status_media_preview_0"
|
||||
|
@ -298,14 +298,17 @@
|
|||
|
||||
<TextView
|
||||
android:id="@+id/status_sensitive_media_warning"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="?attr/sensitive_media_warning_background_color"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/media_warning_bg"
|
||||
android:gravity="center"
|
||||
android:importantForAccessibility="no"
|
||||
android:lineSpacingMultiplier="1.2"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp"
|
||||
android:paddingLeft="12dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingRight="12dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:textAlignment="center"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
|
@ -313,7 +316,9 @@
|
|||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="@string/status_sensitive_media_title"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/status_media_label_0"
|
||||
|
|
|
@ -313,9 +313,9 @@
|
|||
|
||||
<TextView
|
||||
android:id="@+id/status_sensitive_media_warning"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="?attr/sensitive_media_warning_background_color"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/media_warning_bg"
|
||||
android:gravity="center"
|
||||
android:lineSpacingMultiplier="1.2"
|
||||
android:orientation="vertical"
|
||||
|
@ -327,7 +327,9 @@
|
|||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="@string/status_sensitive_media_title"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/status_media_label_0"
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
<item name="status_favourite_active_drawable">@drawable/favourite_active_dark</item>
|
||||
<item name="status_favourite_inactive_drawable">@drawable/favourite_inactive_dark</item>
|
||||
<item name="content_warning_button">@drawable/toggle_small</item>
|
||||
<item name="sensitive_media_warning_background_color">@color/color_background_dark</item>
|
||||
<item name="sensitive_media_warning_background_color">#80000000</item>
|
||||
<item name="media_preview_unloaded_drawable">@drawable/media_preview_unloaded_dark</item>
|
||||
<item name="android:listDivider">@drawable/status_divider_dark</item>
|
||||
<item name="conversation_thread_line_drawable">@drawable/conversation_thread_line_dark</item>
|
||||
|
|
|
@ -227,6 +227,7 @@
|
|||
<string name="pref_title_language">Language</string>
|
||||
<string name="pref_title_bot_overlay">Show indicator for bots</string>
|
||||
<string name="pref_title_animate_gif_avatars">Animate GIF avatars</string>
|
||||
<string name="gradient_for_media">Show colorful gradients for hidden media</string>
|
||||
|
||||
<string name="pref_title_status_filter">Timeline filtering</string>
|
||||
<string name="pref_title_status_tabs">Tabs</string>
|
||||
|
|
|
@ -88,9 +88,7 @@
|
|||
<item name="status_favourite_active_drawable">@drawable/favourite_active_light</item>
|
||||
<item name="status_favourite_inactive_drawable">@drawable/favourite_inactive_light</item>
|
||||
<item name="content_warning_button">@drawable/toggle_small_light</item>
|
||||
<item name="sensitive_media_warning_background_color">
|
||||
@color/sensitive_media_warning_background_light
|
||||
</item>
|
||||
<item name="sensitive_media_warning_background_color">#80B0B0B0</item>
|
||||
<item name="media_preview_unloaded_drawable">@drawable/media_preview_unloaded_light</item>
|
||||
<item name="android:listDivider">@drawable/status_divider_light</item>
|
||||
<item name="conversation_thread_line_drawable">@drawable/conversation_thread_line_light
|
||||
|
@ -189,7 +187,7 @@
|
|||
|
||||
<item name="recents_background_color">@color/toolbar_background_black</item>
|
||||
<item name="window_background">@color/window_background_black</item>
|
||||
<item name="sensitive_media_warning_background_color">@color/color_background_black</item>
|
||||
<item name="sensitive_media_warning_background_color">#80000000</item>
|
||||
<item name="account_header_background_color">@color/color_background_black</item>
|
||||
|
||||
<item name="report_status_background_color">@color/color_background_black</item>
|
||||
|
|
|
@ -60,6 +60,12 @@
|
|||
android:title="@string/pref_title_animate_gif_avatars"
|
||||
app:singleLineTitle="false" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="useBlurhash"
|
||||
android:title="@string/gradient_for_media"
|
||||
app:singleLineTitle="false" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="showNotificationsFilter"
|
||||
|
|
Loading…
Reference in a new issue