chinwag-android/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt
Ivan Kupalov 7623962a0d Use blurhash as image preview and as sensitive media cover, close #1571 (#1581)
* 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
2019-12-30 21:37:20 +01:00

334 lines
No EOL
14 KiB
Kotlin

/* Copyright 2019 Joel Pyska
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
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
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.DrawableRes
import androidx.appcompat.content.res.AppCompatResources
import com.bumptech.glide.Glide
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.view.MediaPreviewImageView
import com.keylesspalace.tusky.viewdata.PollViewData
import com.keylesspalace.tusky.viewdata.buildDescription
import com.keylesspalace.tusky.viewdata.calculatePercent
import java.text.NumberFormat
import java.text.SimpleDateFormat
import java.util.*
import kotlin.math.min
class StatusViewHelper(private val itemView: View) {
interface MediaPreviewListener {
fun onViewMedia(v: View?, idx: Int)
fun onContentHiddenChange(isShowing: Boolean)
}
private val shortSdf = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
private val longSdf = SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault())
fun setMediasPreview(
statusDisplayOptions: StatusDisplayOptions,
attachments: List<Attachment>,
sensitive: Boolean,
previewListener: MediaPreviewListener,
showingContent: Boolean,
mediaPreviewHeight: Int) {
val context = itemView.context
val mediaPreviews = arrayOf<MediaPreviewImageView>(
itemView.findViewById(R.id.status_media_preview_0),
itemView.findViewById(R.id.status_media_preview_1),
itemView.findViewById(R.id.status_media_preview_2),
itemView.findViewById(R.id.status_media_preview_3))
val mediaOverlays = arrayOf<ImageView>(
itemView.findViewById(R.id.status_media_overlay_0),
itemView.findViewById(R.id.status_media_overlay_1),
itemView.findViewById(R.id.status_media_overlay_2),
itemView.findViewById(R.id.status_media_overlay_3))
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 (statusDisplayOptions.mediaPreviewEnabled) {
// Hide the unused label.
mediaLabel.visibility = View.GONE
} else {
setMediaLabel(mediaLabel, attachments, sensitive, previewListener)
// Hide all unused views.
mediaPreviews[0].visibility = View.GONE
mediaPreviews[1].visibility = View.GONE
mediaPreviews[2].visibility = View.GONE
mediaPreviews[3].visibility = View.GONE
sensitiveMediaWarning.visibility = View.GONE
sensitiveMediaShow.visibility = View.GONE
return
}
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 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)
} else {
mediaPreviews[i].contentDescription = description
}
mediaPreviews[i].visibility = View.VISIBLE
if (TextUtils.isEmpty(previewUrl)) {
Glide.with(mediaPreviews[i])
.load(mediaPreviewUnloaded)
.centerInside()
.into(mediaPreviews[i])
} else {
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(placeholder)
.centerInside()
.addListener(mediaPreviews[i])
.into(mediaPreviews[i])
} else {
mediaPreviews[i].removeFocalPoint()
Glide.with(mediaPreviews[i])
.load(previewUrl)
.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 = attachment.type
if (showingContent
&& (type === Attachment.Type.VIDEO) or (type === Attachment.Type.GIFV)) {
mediaOverlays[i].visibility = View.VISIBLE
} else {
mediaOverlays[i].visibility = View.GONE
}
mediaPreviews[i].setOnClickListener { v ->
previewListener.onViewMedia(v, i)
}
if (n <= 2) {
mediaPreviews[0].layoutParams.height = mediaPreviewHeight * 2
mediaPreviews[1].layoutParams.height = mediaPreviewHeight * 2
} else {
mediaPreviews[0].layoutParams.height = mediaPreviewHeight
mediaPreviews[1].layoutParams.height = mediaPreviewHeight
mediaPreviews[2].layoutParams.height = mediaPreviewHeight
mediaPreviews[3].layoutParams.height = mediaPreviewHeight
}
}
if (attachments.isNullOrEmpty()) {
sensitiveMediaWarning.visibility = View.GONE
sensitiveMediaShow.visibility = View.GONE
} else {
val hiddenContentText: String = if (sensitive) {
context.getString(R.string.status_sensitive_media_title)
} else {
context.getString(R.string.status_media_hidden_title)
}
sensitiveMediaWarning.text = HtmlUtils.fromHtml(hiddenContentText)
sensitiveMediaWarning.visibility = if (showingContent) View.GONE else View.VISIBLE
sensitiveMediaShow.visibility = if (showingContent) View.VISIBLE else View.GONE
sensitiveMediaShow.setOnClickListener { v ->
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)
}
}
// Hide any of the placeholder previews beyond the ones set.
for (i in n until Status.MAX_MEDIA_ATTACHMENTS) {
mediaPreviews[i].visibility = View.GONE
}
}
private fun setMediaLabel(mediaLabel: TextView, attachments: List<Attachment>, sensitive: Boolean,
listener: MediaPreviewListener) {
if (attachments.isEmpty()) {
mediaLabel.visibility = View.GONE
return
}
mediaLabel.visibility = View.VISIBLE
// Set the label's text.
val context = mediaLabel.context
var labelText = getLabelTypeText(context, attachments[0].type)
if (sensitive) {
val sensitiveText = context.getString(R.string.status_sensitive_media_title)
labelText += String.format(" (%s)", sensitiveText)
}
mediaLabel.text = labelText
// Set the icon next to the label.
val drawableId = getLabelIcon(attachments[0].type)
val drawable = AppCompatResources.getDrawable(context, drawableId)
ThemeUtils.setDrawableTint(context, drawable!!, android.R.attr.textColorTertiary)
mediaLabel.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null)
mediaLabel.setOnClickListener { listener.onViewMedia(null, 0) }
}
private fun getLabelTypeText(context: Context, type: Attachment.Type): String {
return when (type) {
Attachment.Type.IMAGE -> context.getString(R.string.status_media_images)
Attachment.Type.GIFV, Attachment.Type.VIDEO -> context.getString(R.string.status_media_video)
else -> context.getString(R.string.status_media_images)
}
}
@DrawableRes
private fun getLabelIcon(type: Attachment.Type): Int {
return when (type) {
Attachment.Type.IMAGE -> R.drawable.ic_photo_24dp
Attachment.Type.GIFV, Attachment.Type.VIDEO -> R.drawable.ic_videocam_24dp
else -> R.drawable.ic_photo_24dp
}
}
fun setupPollReadonly(poll: PollViewData?, emojis: List<Emoji>, useAbsoluteTime: Boolean) {
val pollResults = listOf<TextView>(
itemView.findViewById(R.id.status_poll_option_result_0),
itemView.findViewById(R.id.status_poll_option_result_1),
itemView.findViewById(R.id.status_poll_option_result_2),
itemView.findViewById(R.id.status_poll_option_result_3))
val pollDescription = itemView.findViewById<TextView>(R.id.status_poll_description)
if (poll == null) {
for (pollResult in pollResults) {
pollResult.visibility = View.GONE
}
pollDescription.visibility = View.GONE
} else {
val timestamp = System.currentTimeMillis()
setupPollResult(poll, emojis, pollResults)
pollDescription.visibility = View.VISIBLE
pollDescription.text = getPollInfoText(timestamp, poll, pollDescription, useAbsoluteTime)
}
}
private fun getPollInfoText(timestamp: Long, poll: PollViewData, pollDescription: TextView, useAbsoluteTime: Boolean): CharSequence {
val context = pollDescription.context
val votes = NumberFormat.getNumberInstance().format(poll.votesCount.toLong())
val votesText = context.resources.getQuantityString(R.plurals.poll_info_votes, poll.votesCount, votes)
val pollDurationInfo: CharSequence
if (poll.expired) {
pollDurationInfo = context.getString(R.string.poll_info_closed)
} else {
if (useAbsoluteTime) {
pollDurationInfo = context.getString(R.string.poll_info_time_absolute, getAbsoluteTime(poll.expiresAt))
} else {
val pollDuration = TimestampUtils.formatPollDuration(context, poll.expiresAt!!.time, timestamp)
pollDurationInfo = context.getString(R.string.poll_info_time_relative, pollDuration)
}
}
return context.getString(R.string.poll_info_format, votesText, pollDurationInfo)
}
private fun setupPollResult(poll: PollViewData, emojis: List<Emoji>, pollResults: List<TextView>) {
val options = poll.options
for (i in 0 until Status.MAX_POLL_OPTIONS) {
if (i < options.size) {
val percent = calculatePercent(options[i].votesCount, poll.votesCount)
val pollOptionText = buildDescription(options[i].title, percent, pollResults[i].context)
pollResults[i].text = CustomEmojiHelper.emojifyText(pollOptionText, emojis, pollResults[i])
pollResults[i].visibility = View.VISIBLE
val level = percent * 100
pollResults[i].background.level = level
} else {
pollResults[i].visibility = View.GONE
}
}
}
fun getAbsoluteTime(time: Date?): String {
return if (time != null) {
if (android.text.format.DateUtils.isToday(time.time)) {
shortSdf.format(time)
} else {
longSdf.format(time)
}
} else {
"??:??:??"
}
}
companion object {
val COLLAPSE_INPUT_FILTER = arrayOf<InputFilter>(SmartLengthInputFilter)
val NO_INPUT_FILTER = arrayOfNulls<InputFilter>(0)
}
}