Fedibirdの横長絵文字を表示するが、表示形式の判別が仮組み状態

This commit is contained in:
tateisu 2023-02-22 00:17:29 +09:00
parent 8a307bcfed
commit 577bc3fae5
40 changed files with 449 additions and 324 deletions

View File

@ -31,6 +31,7 @@ import jp.juggler.subwaytooter.dialog.actionsDialog
import jp.juggler.subwaytooter.notification.*
import jp.juggler.subwaytooter.push.PushBase
import jp.juggler.subwaytooter.push.pushRepo
import jp.juggler.subwaytooter.span.emojiSizeMode
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.daoAcctColor
import jp.juggler.subwaytooter.table.daoSavedAccount
@ -145,8 +146,8 @@ class ActAccountSetting : AppCompatActivity(),
private var loadingBusy = false
private var profileBusy = false
private lateinit var listEtFieldName: List<EditText>
private lateinit var listEtFieldValue: List<EditText>
// private lateinit var listEtFieldName: List<EditText>
// private lateinit var listEtFieldValue: List<EditText>
private lateinit var listFieldNameInvalidator: List<NetworkEmojiInvalidator>
private lateinit var listFieldValueInvalidator: List<NetworkEmojiInvalidator>
private lateinit var btnFields: View
@ -334,34 +335,31 @@ class ActAccountSetting : AppCompatActivity(),
setDropDownViewResource(R.layout.lv_spinner_dropdown)
}
listEtFieldName = intArrayOf(
listFieldNameInvalidator = intArrayOf(
R.id.etFieldName1,
R.id.etFieldName2,
R.id.etFieldName3,
R.id.etFieldName4
).map { findViewById(it) }
).map {
NetworkEmojiInvalidator(handler, findViewById<EditText>(it))
listEtFieldValue = intArrayOf(
}
listFieldValueInvalidator = intArrayOf(
R.id.etFieldValue1,
R.id.etFieldValue2,
R.id.etFieldValue3,
R.id.etFieldValue4
).map { findViewById(it) }
).map {
NetworkEmojiInvalidator(handler, findViewById<EditText>(it))
}
// btnNotificationStyleEditReply.vg(PrefB.bpSeparateReplyNotificationGroup.value)
// invalidaterがないと描画できないので
nameInvalidator = NetworkEmojiInvalidator(handler, etDisplayName)
noteInvalidator = NetworkEmojiInvalidator(handler, etNote)
defaultTextInvalidator = NetworkEmojiInvalidator(handler, etDefaultText)
listFieldNameInvalidator = listEtFieldName.map {
NetworkEmojiInvalidator(handler, it)
}
listFieldValueInvalidator = listEtFieldValue.map {
NetworkEmojiInvalidator(handler, it)
}
val watcher1 = simpleTextWatcher {
saveUIToData()
}
@ -433,7 +431,7 @@ class ActAccountSetting : AppCompatActivity(),
swNotificationPullEnabled.isChecked = a.notificationPullEnable
swNotificationPushEnabled.isChecked = a.notificationPushEnable
etDefaultText.setText(a.defaultText)
defaultTextInvalidator.text = a.defaultText
etMaxTootChars.setText(a.maxTootChars.toString())
val ti = TootInstance.getCached(a)
@ -884,8 +882,8 @@ class ActAccountSetting : AppCompatActivity(),
ivProfileAvatar.setErrorImage(defaultColorIcon(this@ActAccountSetting, questionId))
ivProfileAvatar.setDefaultImage(defaultColorIcon(this@ActAccountSetting, questionId))
etDisplayName.setText(loadingText)
etNote.setText(loadingText)
nameInvalidator.text = loadingText
noteInvalidator.text = loadingText
// 初期状態では編集不可能
arrayOf(
@ -898,13 +896,15 @@ class ActAccountSetting : AppCompatActivity(),
cbLocked,
).forEach { it.isEnabledAlpha = false }
for (et in listEtFieldName) {
et.setText(loadingText)
et.isEnabledAlpha = false
for (i in listFieldNameInvalidator) {
i.text = loadingText
i.view.isEnabledAlpha = false
}
for (et in listEtFieldValue) {
et.setText(loadingText)
et.isEnabledAlpha = false
for (i in listFieldValueInvalidator) {
i.text = loadingText
i.view.isEnabledAlpha = false
}
// 疑似アカウントなら編集不可のまま
@ -968,12 +968,12 @@ class ActAccountSetting : AppCompatActivity(),
emojiMapProfile = src.profile_emojis,
emojiMapCustom = src.custom_emojis,
authorDomain = account,
emojiSizeMode = account.emojiSizeMode(),
)
val displayName = src.display_name
val name = decodeOptions.decodeEmoji(displayName)
views.etDisplayName.setText(name)
nameInvalidator.register(name)
nameInvalidator.text = name
val noteString = src.source?.note ?: src.note
val noteSpannable = when {
@ -986,8 +986,7 @@ class ActAccountSetting : AppCompatActivity(),
}
}
views.etNote.setText(noteSpannable)
noteInvalidator.register(noteSpannable)
noteInvalidator.text = noteSpannable
views.cbLocked.isChecked = src.locked
@ -1006,75 +1005,55 @@ class ActAccountSetting : AppCompatActivity(),
if (src.source?.fields != null) {
val fields = src.source.fields
listEtFieldName.forEachIndexed { i, et ->
val handler = et.handler // may null
if (handler != null) {
// いつからかfields name にもカスタム絵文字が使えるようになった
// https://github.com/tootsuite/mastodon/pull/11350
// しかし
val text = decodeOptions.decodeEmoji(
when {
i >= fields.size -> ""
else -> fields[i].name
}
)
et.setText(text)
et.isEnabledAlpha = true
val invalidator = NetworkEmojiInvalidator(handler, et)
invalidator.register(text)
}
listFieldNameInvalidator.forEachIndexed { i, et ->
// いつからかfields name にもカスタム絵文字が使えるようになった
// https://github.com/tootsuite/mastodon/pull/11350
// しかし
val text = decodeOptions.decodeEmoji(
when {
i >= fields.size -> ""
else -> fields[i].name
}
)
et.text = text
et.view.isEnabledAlpha = true
}
listEtFieldValue.forEachIndexed { i, et ->
val handler = et.handler // may null
if (handler != null) {
val text = decodeOptions.decodeEmoji(
when {
i >= fields.size -> ""
else -> fields[i].value
}
)
et.setText(text)
et.isEnabledAlpha = true
val invalidator = NetworkEmojiInvalidator(handler, et)
invalidator.register(text)
}
listFieldValueInvalidator.forEachIndexed { i, et ->
val text = decodeOptions.decodeEmoji(
when {
i >= fields.size -> ""
else -> fields[i].value
}
)
et.text = text
et.view.isEnabledAlpha = true
}
} else {
val fields = src.fields
listEtFieldName.forEachIndexed { i, et ->
val handler = et.handler // may null
if (handler != null) {
// いつからかfields name にもカスタム絵文字が使えるようになった
// https://github.com/tootsuite/mastodon/pull/11350
val text = decodeOptions.decodeEmoji(
when {
fields == null || i >= fields.size -> ""
else -> fields[i].name
}
)
et.setText(text)
et.isEnabledAlpha = true
val invalidator = NetworkEmojiInvalidator(handler, et)
invalidator.register(text)
}
listFieldNameInvalidator.forEachIndexed { i, et ->
// いつからかfields name にもカスタム絵文字が使えるようになった
// https://github.com/tootsuite/mastodon/pull/11350
val text = decodeOptions.decodeEmoji(
when {
fields == null || i >= fields.size -> ""
else -> fields[i].name
}
)
et.text = text
et.view.isEnabledAlpha = true
}
listEtFieldValue.forEachIndexed { i, et ->
val handler = et.handler // may null
if (handler != null) {
val text = decodeOptions.decodeHTML(
when {
fields == null || i >= fields.size -> ""
else -> fields[i].value
}
)
et.text = text
et.isEnabledAlpha = true
val invalidator = NetworkEmojiInvalidator(handler, et)
invalidator.register(text)
}
listFieldValueInvalidator.forEachIndexed { i, et ->
val text = decodeOptions.decodeHTML(
when {
fields == null || i >= fields.size -> ""
else -> fields[i].value
}
)
et.text = text
et.view.isEnabledAlpha = true
}
}
} finally {
@ -1314,9 +1293,9 @@ class ActAccountSetting : AppCompatActivity(),
private fun sendFields(bConfirmed: Boolean = false) {
val args = ArrayList<Pair<String, String>>()
var lengthLongest = -1
for (i in listEtFieldName.indices) {
val k = listEtFieldName[i].text.toString().trim()
val v = listEtFieldValue[i].text.toString().trim()
for (i in listFieldNameInvalidator.indices) {
val k = listFieldNameInvalidator[i].text.toString().trim()
val v = listFieldValueInvalidator[i].text.toString().trim()
args.add(Pair("fields_attributes[$i][name]", k))
args.add(Pair("fields_attributes[$i][value]", v))

View File

@ -11,6 +11,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import jp.juggler.subwaytooter.databinding.ActGlideTestBinding
import jp.juggler.subwaytooter.databinding.LvGlideTestBinding
import jp.juggler.subwaytooter.span.EmojiSizeMode
import jp.juggler.subwaytooter.span.NetworkEmojiSpan
import jp.juggler.subwaytooter.util.NetworkEmojiInvalidator
import jp.juggler.util.coroutine.AppDispatchers
@ -98,13 +99,13 @@ class ActGlideTest : AppCompatActivity() {
val span = NetworkEmojiSpan(
url = item.url,
scale = 2f,
sizeMode = EmojiSizeMode.Square,
)
setSpan(span, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
append(" ")
append(item.name)
}
nameInvalidator.register(text)
views.tvName.text = text
nameInvalidator.text = text
}
}

View File

@ -18,6 +18,7 @@ import jp.juggler.subwaytooter.dialog.launchEmojiPicker
import jp.juggler.subwaytooter.dialog.pickAccount
import jp.juggler.subwaytooter.emoji.CustomEmoji
import jp.juggler.subwaytooter.emoji.UnicodeEmoji
import jp.juggler.subwaytooter.span.emojiSizeMode
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.accountListCanReaction
import jp.juggler.subwaytooter.table.daoAcctColor
@ -120,7 +121,8 @@ fun ActMain.reactionAdd(
accessInfo,
decodeEmoji = true,
enlargeEmoji = 1.5f,
enlargeCustomEmoji = 1.5f
enlargeCustomEmoji = 1.5f,
emojiSizeMode = accessInfo.emojiSizeMode(),
)
val emojiSpan = TootReaction.toSpannableStringBuilder(options, code, urlArg)
confirm(
@ -218,7 +220,8 @@ fun ActMain.reactionRemove(
accessInfo,
decodeEmoji = true,
enlargeEmoji = 1.5f,
enlargeCustomEmoji = 1.5f
enlargeCustomEmoji = 1.5f,
emojiSizeMode = accessInfo.emojiSizeMode(),
)
val emojiSpan = reaction.toSpannableStringBuilder(options, status)
confirm(R.string.reaction_remove_confirm, emojiSpan)
@ -328,7 +331,8 @@ private fun ActMain.reactionWithoutUi(
accessInfo,
decodeEmoji = true,
enlargeEmoji = 1.5f,
enlargeCustomEmoji = 1.5f
enlargeCustomEmoji = 1.5f,
emojiSizeMode = accessInfo.emojiSizeMode(),
)
val emojiSpan = TootReaction.toSpannableStringBuilder(options, reactionCode, reactionImage)
val isCustomEmoji = TootReaction.isCustomEmoji(reactionCode)

View File

@ -7,18 +7,31 @@ import jp.juggler.subwaytooter.ActMain
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.pref.PrefS
import jp.juggler.subwaytooter.span.NetworkEmojiSpan
import jp.juggler.subwaytooter.view.MyTextView
import java.lang.ref.WeakReference
// AutoCWの基準幅を計算する
fun ActMain.resizeAutoCW(columnW: Int) {
/**
* 各カラムの各投稿のコンテンツ部分の幅を覚える
*/
fun ActMain.saveContentTextWidth(columnW: Int) {
val lvPad = (0.5f + 12 * density).toInt()
val iconWidth = avatarIconSize
val iconEnd = (0.5f + 4 * density).toInt()
val contentTextWidth = columnW - lvPad * 2 - iconWidth - iconEnd
// CW部分の絵文字とか、リアクションなら数字と枠を避けたサイズとか
val maxEmojiWidth = contentTextWidth - (64f * density+ 0.5f )
// NetworkEmojiSpanに覚える
NetworkEmojiSpan.maxEmojiWidth = maxEmojiWidth
// 自動CW用の値に覚える
val sv = PrefS.spAutoCWLines.value
nAutoCwLines = sv.toIntOrNull() ?: -1
if (nAutoCwLines > 0) {
val lvPad = (0.5f + 12 * density).toInt()
val iconWidth = avatarIconSize
val iconEnd = (0.5f + 4 * density).toInt()
nAutoCwCellWidth = columnW - lvPad * 2 - iconWidth - iconEnd
nAutoCwCellWidth = contentTextWidth
}
// この後各カラムは再描画される
}

View File

@ -445,7 +445,7 @@ fun ActMain.resizeColumnWidth(views: ActMainTabletViews) {
views.tabletPagerAdapter.columnWidth = columnW // dividerの幅を含まない
// env.tablet_snap_helper.columnWidth = column_w //使われていない
resizeAutoCW(columnW) // dividerの幅を含まない
saveContentTextWidth(columnW) // dividerの幅を含まない
// 並べ直す
views.tabletPagerAdapter.notifyDataSetChanged()

View File

@ -37,7 +37,7 @@ fun ActMain.initPhoneTablet() {
phoneViews = ActMainPhoneViews(this).apply {
initUI(tmpPhonePager)
}
resizeAutoCW(sw)
saveContentTextWidth(sw)
} else {
tmpPhonePager.visibility = View.GONE
tabletViews = ActMainTabletViews(this).apply {

View File

@ -10,6 +10,7 @@ import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.entity.TootAttachment.Companion.tootAttachmentJson
import jp.juggler.subwaytooter.dialog.DlgDraftPicker
import jp.juggler.subwaytooter.span.emojiSizeMode
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.daoPostDraft
import jp.juggler.subwaytooter.table.daoSavedAccount

View File

@ -13,6 +13,7 @@ import jp.juggler.subwaytooter.api.entity.unknownHostAndDomain
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
import jp.juggler.subwaytooter.dialog.actionsDialog
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.span.emojiSizeMode
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.daoPostDraft
import jp.juggler.subwaytooter.table.daoSavedAccount
@ -39,6 +40,7 @@ suspend fun ActPost.appendContentText(
context = this,
decodeEmoji = true,
authorDomain = account ?: unknownHostAndDomain,
emojiSizeMode = account.emojiSizeMode(),
).decodeEmoji(src)
if (svEmoji.isEmpty()) return

View File

@ -10,6 +10,7 @@ import jp.juggler.subwaytooter.api.entity.TootVisibility
import jp.juggler.subwaytooter.api.runApiTask
import jp.juggler.subwaytooter.api.syncStatus
import jp.juggler.subwaytooter.calcIconRound
import jp.juggler.subwaytooter.span.emojiSizeMode
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.util.DecodeOptions
import jp.juggler.util.coroutine.launchMain
@ -38,6 +39,7 @@ suspend fun ActPost.showReplyTo() {
linkHelper = account,
short = true,
decodeEmoji = true,
emojiSizeMode = account.emojiSizeMode(),
).decodeHTML(states.inReplyToText)
views.ivReply.setImageUrl(
calcIconRound(views.ivReply.layoutParams),

View File

@ -15,6 +15,7 @@ import jp.juggler.subwaytooter.emoji.EmojiBase
import jp.juggler.subwaytooter.emoji.UnicodeEmoji
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.span.NetworkEmojiSpan
import jp.juggler.subwaytooter.span.emojiSizeMode
import jp.juggler.subwaytooter.table.*
import jp.juggler.subwaytooter.util.DecodeOptions
import jp.juggler.subwaytooter.util.EmojiDecoder
@ -274,7 +275,7 @@ class CompletionHelper(
val sb = SpannableStringBuilder()
sb.append(' ')
sb.setSpan(
NetworkEmojiSpan(item.url),
NetworkEmojiSpan(item.url, sizeMode = accessInfo.emojiSizeMode()),
0,
sb.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
@ -407,12 +408,12 @@ class CompletionHelper(
// et.setCustomSelectionActionModeCallback( action_mode_callback );
}
private suspend fun SpannableStringBuilder.appendEmoji(
private fun SpannableStringBuilder.appendEmoji(
emoji: EmojiBase,
bInstanceHasCustomEmoji: Boolean,
) = appendEmoji(bInstanceHasCustomEmoji, emoji)
private suspend fun SpannableStringBuilder.appendEmoji(
private fun SpannableStringBuilder.appendEmoji(
bInstanceHasCustomEmoji: Boolean,
emoji: EmojiBase,
): SpannableStringBuilder {

View File

@ -4,6 +4,9 @@ import android.content.Context
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.entity.TootAccount.Companion.tootAccount
import jp.juggler.subwaytooter.api.entity.TootStatus.Companion.tootStatus
import jp.juggler.subwaytooter.span.EmojiSizeMode
import jp.juggler.subwaytooter.span.emojiSizeMode
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.UserRelation
import jp.juggler.subwaytooter.util.LinkHelper
import jp.juggler.util.data.JsonArray
@ -67,4 +70,7 @@ class TootParser(
else -> null
}
}
val emojiSizeMode: EmojiSizeMode
get()= (linkHelper as? SavedAccount).emojiSizeMode()
}

View File

@ -3,7 +3,6 @@ package jp.juggler.subwaytooter.api.entity
import android.content.Context
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.widget.TextView
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.MisskeyAccountDetailMap
import jp.juggler.subwaytooter.api.TootParser
@ -11,6 +10,7 @@ import jp.juggler.subwaytooter.api.entity.TootAccountRef.Companion.tootAccountRe
import jp.juggler.subwaytooter.api.entity.TootStatus.Companion.tootStatus
import jp.juggler.subwaytooter.emoji.CustomEmoji
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.span.emojiSizeMode
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.daoUserRelation
import jp.juggler.subwaytooter.util.DecodeOptions
@ -167,7 +167,7 @@ open class TootAccount(
context,
emojiMapProfile = profile_emojis,
emojiMapCustom = custom_emojis,
authorDomain = this
authorDomain = this,
).decodeEmoji(sv)
}
@ -182,7 +182,7 @@ open class TootAccount(
context,
emojiMapProfile = profile_emojis,
emojiMapCustom = custom_emojis,
authorDomain = this
authorDomain = this,
).decodeEmoji(sv)
}
@ -219,12 +219,11 @@ open class TootAccount(
fun setAccountExtra(
accessInfo: SavedAccount,
tv: TextView,
invalidator: NetworkEmojiInvalidator?,
invalidator: NetworkEmojiInvalidator,
fromProfileHeader: Boolean = false,
suggestionSource: String? = null,
): SpannableStringBuilder? {
val context = tv.context
val context = invalidator.view.context
var sb: SpannableStringBuilder? = null
fun prepareSb() = sb?.apply { append('\n') } ?: SpannableStringBuilder().also { sb = it }
@ -281,6 +280,7 @@ open class TootAccount(
emojiMapCustom = custom_emojis,
unwrapEmojiImageTag = true,
authorDomain = this,
emojiSizeMode = accessInfo.emojiSizeMode(),
).decodeHTML(note)
.replaceAllEx(reNoteLineFeed, " ")
.trimEx()
@ -296,13 +296,10 @@ open class TootAccount(
}
}
tv.vg(sb != null)
?.apply {
text = sb
movementMethod = MyLinkMovementMethod
invalidator?.register(sb)
}
?: invalidator?.clear()
invalidator.view.vg(sb != null)?.apply {
invalidator.text = sb!!
movementMethod = MyLinkMovementMethod
} ?: invalidator.clear()
return sb
}

View File

@ -49,6 +49,7 @@ class TootAccountRef private constructor(
emojiMapCustom = account.custom_emojis,
unwrapEmojiImageTag = true,
authorDomain = account,
emojiSizeMode = parser.emojiSizeMode,
).decodeHTML(account.note),
)
}

View File

@ -3,6 +3,7 @@ package jp.juggler.subwaytooter.api.entity
import android.text.Spannable
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.emoji.CustomEmoji
import jp.juggler.subwaytooter.span.emojiSizeMode
import jp.juggler.subwaytooter.util.DecodeOptions
import jp.juggler.util.data.JsonObject
import jp.juggler.util.data.notEmpty
@ -55,6 +56,7 @@ class TootAnnouncement(
// attachmentList = media_attachments,
highlightTrie = parser.highlightTrie,
mentions = mentions,
emojiSizeMode = parser.emojiSizeMode,
)
val content = src.string("content") ?: ""
return TootAnnouncement(

View File

@ -1,6 +1,7 @@
package jp.juggler.subwaytooter.api.entity
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.span.emojiSizeMode
import jp.juggler.subwaytooter.util.DecodeOptions
import jp.juggler.util.data.JsonObject
import jp.juggler.util.data.filterNotEmpty
@ -58,6 +59,7 @@ class TootCard(
context = parser.context,
decodeEmoji = true,
authorDomain = src.account,
emojiSizeMode = parser.emojiSizeMode,
).decodeHTML(src.content ?: "").toString()
},
image = src.media_attachments

View File

@ -126,7 +126,8 @@ class TootPolls(
emojiMapCustom = status.custom_emojis,
emojiMapProfile = status.profile_emojis,
mentions = status.mentions,
authorDomain = status.account
authorDomain = status.account,
emojiSizeMode = parser.emojiSizeMode,
).decodeHTML(question ?: "?"),
)
}
@ -134,7 +135,7 @@ class TootPolls(
TootPollsType.Mastodon -> {
val question = status.content
val items = parseChoiceListMastodon(
parser.context,
parser,
status,
src.jsonArray("options")?.objectList()
)
@ -186,7 +187,8 @@ class TootPolls(
emojiMapCustom = status.custom_emojis,
emojiMapProfile = status.profile_emojis,
mentions = status.mentions,
authorDomain = status.account
authorDomain = status.account,
emojiSizeMode = parser.emojiSizeMode,
).decodeHTML(question ?: "?"),
pollId = EntityId.mayNull(src.string("id")),
expired_at =
@ -214,10 +216,11 @@ class TootPolls(
emojiMapCustom = status.custom_emojis,
emojiMapProfile = status.profile_emojis,
mentions = status.mentions,
authorDomain = status.account
authorDomain = status.account,
emojiSizeMode = parser.emojiSizeMode,
).decodeHTML(question ?: "?"),
items = parseChoiceListFriendsNico(
parser.context,
parser,
status,
src.stringArrayList("items")
),
@ -232,7 +235,7 @@ class TootPolls(
val expired_at =
TootStatus.parseTime(src.string("endTime")).notZero() ?: Long.MAX_VALUE
val items = parseChoiceListNotestock(
parser.context,
parser,
status,
srcArray?.objectList()
)
@ -281,6 +284,7 @@ class TootPolls(
mentions = status.mentions,
authorDomain = status.account,
unwrapEmojiImageTag = true, // notestockはカスタム絵文字がimageタグになってる
emojiSizeMode = parser.emojiSizeMode,
).decodeHTML(question ?: "?"),
pollId = EntityId.DEFAULT,
expired = expired_at >= System.currentTimeMillis(),
@ -313,7 +317,7 @@ class TootPolls(
}
private fun parseChoiceListMastodon(
context: Context,
parser: TootParser,
status: TootStatus,
objectArray: List<JsonObject>?,
): ArrayList<TootPollsChoice>? {
@ -321,11 +325,13 @@ class TootPolls(
val size = objectArray.size
val items = ArrayList<TootPollsChoice>(size)
val options = DecodeOptions(
context,
context = parser.context,
linkHelper = parser.linkHelper,
emojiMapCustom = status.custom_emojis,
emojiMapProfile = status.profile_emojis,
decodeEmoji = true,
authorDomain = status.account
authorDomain = status.account,
emojiSizeMode = parser.emojiSizeMode,
)
for (o in objectArray) {
val text = reWhitespace
@ -347,7 +353,7 @@ class TootPolls(
}
private fun parseChoiceListNotestock(
context: Context,
parser: TootParser,
status: TootStatus,
objectArray: List<JsonObject>?,
): ArrayList<TootPollsChoice>? {
@ -355,11 +361,13 @@ class TootPolls(
val size = objectArray.size
val items = ArrayList<TootPollsChoice>(size)
val options = DecodeOptions(
context,
context = parser.context,
linkHelper = parser.linkHelper,
emojiMapCustom = status.custom_emojis,
emojiMapProfile = status.profile_emojis,
decodeEmoji = true,
authorDomain = status.account
authorDomain = status.account,
emojiSizeMode = parser.emojiSizeMode,
)
for (o in objectArray) {
val text = reWhitespace
@ -381,7 +389,7 @@ class TootPolls(
}
private fun parseChoiceListFriendsNico(
context: Context,
parser: TootParser,
status: TootStatus,
stringArray: ArrayList<String>?,
): ArrayList<TootPollsChoice>? {
@ -389,11 +397,13 @@ class TootPolls(
val size = stringArray.size
val items = ArrayList<TootPollsChoice>(size)
val options = DecodeOptions(
context,
context = parser.context,
linkHelper = parser.linkHelper,
emojiMapCustom = status.custom_emojis,
emojiMapProfile = status.profile_emojis,
decodeEmoji = true,
authorDomain = status.account
authorDomain = status.account,
emojiSizeMode = parser.emojiSizeMode,
)
for (i in 0 until size) {
val text = reWhitespace

View File

@ -126,15 +126,22 @@ class TootReaction(
fun encodeEmojiQuery(src: List<TootReaction>): String =
JsonArray(src.map { it.jsonFedibird() } as Collection<JsonObject>).toString()
fun urlToSpan(options: DecodeOptions, code: String, url: String) =
SpannableStringBuilder(code).apply {
setSpan(
NetworkEmojiSpan(url, scale = options.enlargeCustomEmoji),
0,
length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
fun urlToSpan(
options: DecodeOptions,
code: String,
url: String,
) = SpannableStringBuilder(code).apply {
setSpan(
NetworkEmojiSpan(
url = url,
scale = options.enlargeCustomEmoji,
sizeMode = options.emojiSizeMode
),
0,
length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
fun toSpannableStringBuilder(
options: DecodeOptions,

View File

@ -618,7 +618,8 @@ class TootStatus(
attachmentList = media_attachments,
highlightTrie = parser.highlightTrie,
mentions = null, // MisskeyはMFMをパースし終わるまでメンションが分からない
authorDomain = accountRef.get()
authorDomain = accountRef.get(),
emojiSizeMode = parser.emojiSizeMode,
)
// ハイライト検出のためにDecodeOptionsを作り直す
val options2 = DecodeOptions(
@ -631,7 +632,8 @@ class TootStatus(
attachmentList = media_attachments,
highlightTrie = parser.highlightTrie,
mentions = null, // MisskeyはMFMをパースし終わるまでメンションが分からない
authorDomain = accountRef.get()
authorDomain = accountRef.get(),
emojiSizeMode = parser.emojiSizeMode,
)
val content = src.string("text")
@ -834,6 +836,7 @@ class TootStatus(
mentions = mentions,
authorDomain = account,
unwrapEmojiImageTag = true, // notestockはカスタム絵文字がimageタグになってる
emojiSizeMode = parser.emojiSizeMode,
)
// ハイライト検出のためにDecodeOptionsを作り直す
@ -845,6 +848,7 @@ class TootStatus(
mentions = mentions,
authorDomain = account,
unwrapEmojiImageTag = true, // notestockはカスタム絵文字がimageタグになってる
emojiSizeMode = parser.emojiSizeMode,
)
val content = src.string("content")
@ -971,7 +975,7 @@ class TootStatus(
val who = parser.account(src.jsonObject("account"))
?: error("missing account")
val accountRef = TootAccountRef.tootAccountRef(parser, who)
val accountRef = tootAccountRef(parser, who)
val account = accountRef.get()
val readerApDomain: Host?
@ -1093,7 +1097,8 @@ class TootStatus(
attachmentList = media_attachments,
highlightTrie = parser.highlightTrie,
mentions = mentions,
authorDomain = account
authorDomain = account,
emojiSizeMode = parser.emojiSizeMode,
)
val decoded_content = options1.decodeHTML(content)
@ -1115,7 +1120,8 @@ class TootStatus(
emojiMapProfile = profile_emojis,
highlightTrie = parser.highlightTrie,
mentions = mentions,
authorDomain = account
authorDomain = account,
emojiSizeMode = parser.emojiSizeMode,
)
val decoded_spoiler_text = options2.decodeEmoji(spoiler_text)
if (highlightSound == null) highlightSound = options2.highlightSound

View File

@ -26,6 +26,7 @@ import jp.juggler.subwaytooter.emoji.CustomEmoji
import jp.juggler.subwaytooter.emoji.UnicodeEmoji
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.span.NetworkEmojiSpan
import jp.juggler.subwaytooter.span.emojiSizeMode
import jp.juggler.subwaytooter.util.*
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain
@ -225,9 +226,8 @@ private fun ColumnViewHolder.showAnnouncementContent(item: TootAnnouncement, con
val sb = periods
tvAnnouncementPeriod.vg(sb != null)?.text = sb
tvAnnouncementContent.textColor = contentColor
tvAnnouncementContent.text = item.decoded_content
tvAnnouncementContent.tag = this
announcementContentInvalidator.register(item.decoded_content)
announcementContentInvalidator.text = item.decoded_content
}
private fun ColumnViewHolder.showReactionBox(
@ -333,7 +333,8 @@ private fun ColumnViewHolder.showReactions(
column.accessInfo,
decodeEmoji = true,
enlargeEmoji = 1.5f,
authorDomain = column.accessInfo
authorDomain = column.accessInfo,
emojiSizeMode = column.accessInfo.emojiSizeMode(),
)
val actMain = activity
@ -377,18 +378,16 @@ private fun ColumnViewHolder.showReactions(
if (url == null) {
btn.text = EmojiDecoder.decodeEmoji(options, "${reaction.name} ${reaction.count}")
} else {
btn.text = SpannableStringBuilder("${reaction.name} ${reaction.count}").also { sb ->
val invalidator = NetworkEmojiInvalidator(actMain.handler, btn)
invalidator.text = SpannableStringBuilder("${reaction.name} ${reaction.count}").also { sb ->
sb.setSpan(
NetworkEmojiSpan(url, scale = 1.5f),
NetworkEmojiSpan(url, scale = 1.5f, sizeMode = options.emojiSizeMode),
0,
reaction.name.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
val invalidator =
NetworkEmojiInvalidator(actMain.handler, btn)
invalidator.register(sb)
extraInvalidatorList.add(invalidator)
}
extraInvalidatorList.add(invalidator)
}
btn.setOnClickListener {

View File

@ -10,6 +10,7 @@ import jp.juggler.subwaytooter.column.getContentColor
import jp.juggler.subwaytooter.dialog.launchEmojiPicker
import jp.juggler.subwaytooter.emoji.CustomEmoji
import jp.juggler.subwaytooter.emoji.UnicodeEmoji
import jp.juggler.subwaytooter.span.emojiSizeMode
import jp.juggler.subwaytooter.util.DecodeOptions
import jp.juggler.subwaytooter.util.NetworkEmojiInvalidator
import jp.juggler.subwaytooter.util.minWidthCompat
@ -65,7 +66,8 @@ fun ColumnViewHolder.updateReactionQueryView() {
column.accessInfo,
decodeEmoji = true,
enlargeEmoji = 1.5f,
enlargeCustomEmoji = 1.5f
enlargeCustomEmoji = 1.5f,
emojiSizeMode = column.accessInfo.emojiSizeMode(),
)
val buttonHeight = ActMain.boostButtonSize
@ -93,8 +95,6 @@ fun ColumnViewHolder.updateReactionQueryView() {
setTextColor(contentColor)
setPadding(paddingH, paddingV, paddingH, paddingV)
text = ssb
allCaps = false
tag = reaction
@ -104,7 +104,7 @@ fun ColumnViewHolder.updateReactionQueryView() {
}
// カスタム絵文字の場合、アニメーション等のコールバックを処理する必要がある
val invalidator = NetworkEmojiInvalidator(act.handler, this)
invalidator.register(ssb)
invalidator.text = ssb
emojiQueryInvalidatorList.add(invalidator)
}
flEmoji.addView(b)

View File

@ -14,6 +14,7 @@ import jp.juggler.subwaytooter.api.entity.TootInstance
import jp.juggler.subwaytooter.column.Column
import jp.juggler.subwaytooter.column.ColumnType
import jp.juggler.subwaytooter.databinding.LvHeaderInstanceBinding
import jp.juggler.subwaytooter.span.emojiSizeMode
import jp.juggler.subwaytooter.util.DecodeOptions
import jp.juggler.subwaytooter.util.openBrowser
import jp.juggler.subwaytooter.util.openCustomTab
@ -128,7 +129,8 @@ internal class ViewHolderHeaderInstance(
activity,
accessInfo,
decodeEmoji = true,
authorDomain = accessInfo
authorDomain = accessInfo,
emojiSizeMode = accessInfo.emojiSizeMode(),
)
tvShortDescription.text = options

View File

@ -26,10 +26,7 @@ import jp.juggler.subwaytooter.emoji.EmojiMap
import jp.juggler.subwaytooter.itemviewholder.DlgContextMenu
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.pref.PrefI
import jp.juggler.subwaytooter.span.EmojiImageSpan
import jp.juggler.subwaytooter.span.LinkInfo
import jp.juggler.subwaytooter.span.MyClickableSpan
import jp.juggler.subwaytooter.span.createSpan
import jp.juggler.subwaytooter.span.*
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.UserRelation
import jp.juggler.subwaytooter.table.daoAcctColor
@ -251,9 +248,9 @@ internal class ViewHolderHeaderProfile(
tvAcct.text = "@"
tvDisplayName.text = ""
nameInvalidator1.text = ""
tvNote.text = ""
noteInvalidator.text = ""
tvMisskeyExtra.text = ""
btnStatusCount.text = activity.getString(R.string.statuses) + "\n" + "?"
@ -276,8 +273,7 @@ internal class ViewHolderHeaderProfile(
who.setAccountExtra(
accessInfo,
tvLastStatusAt,
invalidator = null,
NetworkEmojiInvalidator(activity.handler ,tvLastStatusAt),
fromProfileHeader = true
)
@ -296,16 +292,14 @@ internal class ViewHolderHeaderProfile(
)
val name = whoDetail?.decodeDisplayName(activity) ?: whoRef.decoded_display_name
tvDisplayName.text = name
nameInvalidator1.register(name)
nameInvalidator1.text = name
tvRemoteProfileWarning.vg(column.accessInfo.isRemoteUser(who))
tvAcct.text = encodeAcctText(who, whoDetail)
val note = whoRef.decoded_note
tvNote.text = note
noteInvalidator.register(note)
noteInvalidator.text = note
tvMisskeyExtra.text = encodeMisskeyExtra(whoDetail)
tvMisskeyExtra.vg(tvMisskeyExtra.text.isNotEmpty())
@ -361,8 +355,7 @@ internal class ViewHolderHeaderProfile(
val caption = who.decodeDisplayName(activity)
.intoStringResource(activity, R.string.account_moved_to)
tvMoved.text = caption
movedCaptionInvalidator.register(caption)
movedCaptionInvalidator.text = caption
ivMoved.layoutParams.width = activity.avatarIconSize
ivMoved.setImageUrl(
@ -370,8 +363,7 @@ internal class ViewHolderHeaderProfile(
accessInfo.supplyBaseUrl(moved.avatar_static)
)
tvMovedName.text = movedRef.decoded_display_name
movedNameInvalidator.register(movedRef.decoded_display_name)
movedNameInvalidator.text = movedRef.decoded_display_name
setAcct(tvMovedAcct, accessInfo, moved)
@ -633,7 +625,8 @@ internal class ViewHolderHeaderProfile(
short = true,
emojiMapCustom = who.custom_emojis,
emojiMapProfile = who.profile_emojis,
authorDomain = who
authorDomain = who,
emojiSizeMode = accessInfo.emojiSizeMode(),
)
val nameTypeface = ActMain.timelineFontBold
@ -648,17 +641,17 @@ internal class ViewHolderHeaderProfile(
LinearLayout.LayoutParams.WRAP_CONTENT
)
val nameText = fieldDecodeOptions.decodeEmoji(item.name)
val nameInvalidator = NetworkEmojiInvalidator(activity.handler, nameView)
nameInvalidator.register(nameText)
nameLp.topMargin = (density * 6f).toInt()
nameView.layoutParams = nameLp
nameView.text = nameText
nameView.setTextColor(colorTextContent)
nameView.typeface = nameTypeface
nameView.movementMethod = MyLinkMovementMethod
views.llFields.addView(nameView)
val nameInvalidator = NetworkEmojiInvalidator(activity.handler, nameView)
nameInvalidator.text = nameText
// 値の方はHTMLエンコードされている
val valueView = MyTextView(activity)
val valueLp = LinearLayout.LayoutParams(
@ -687,16 +680,16 @@ internal class ViewHolderHeaderProfile(
)
}
val valueInvalidator = NetworkEmojiInvalidator(activity.handler, valueView)
valueInvalidator.register(valueText)
valueLp.startMargin = (density * 32f).toInt()
valueView.layoutParams = valueLp
valueView.text = valueText
valueView.setTextColor(colorTextContent)
valueView.typeface = valueTypeface
valueView.movementMethod = MyLinkMovementMethod
val valueInvalidator = NetworkEmojiInvalidator(activity.handler, valueView)
valueInvalidator.text = valueText
if (item.verified_at > 0L) {
val linkBgColor = PrefI.ipVerifiedLinkBgColor.value.notZero()
?: (0x337fbc99)

View File

@ -7,6 +7,7 @@ import jp.juggler.subwaytooter.column.Column
import jp.juggler.subwaytooter.column.getContentColor
import jp.juggler.subwaytooter.column.getHeaderDesc
import jp.juggler.subwaytooter.databinding.LvHeaderSearchDescBinding
import jp.juggler.subwaytooter.span.emojiSizeMode
import jp.juggler.subwaytooter.util.DecodeOptions
import jp.juggler.subwaytooter.view.MyLinkMovementMethod
import org.jetbrains.anko.textColor
@ -35,7 +36,8 @@ internal class ViewHolderHeaderSearch(
tvSearchDesc.textColor = column.getContentColor()
tvSearchDesc.text = DecodeOptions(
activity, accessInfo, decodeEmoji = true,
authorDomain = accessInfo
authorDomain = accessInfo,
emojiSizeMode = accessInfo.emojiSizeMode(),
).decodeHTML(column.getHeaderDesc())
}
}

View File

@ -13,6 +13,7 @@ import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.auth.CreateUserParams
import jp.juggler.subwaytooter.api.entity.Host
import jp.juggler.subwaytooter.api.entity.TootInstance
import jp.juggler.subwaytooter.span.emojiSizeMode
import jp.juggler.subwaytooter.util.DecodeOptions
import jp.juggler.subwaytooter.util.LinkHelper
import jp.juggler.subwaytooter.util.openCustomTab
@ -69,7 +70,7 @@ class DlgCreateAccount(
linkHelper = LinkHelper.create(
apiHost,
misskeyVersion = instanceInfo?.misskeyVersionMajor ?: 0
)
),
).decodeHTML(
instanceInfo?.short_description?.notBlank()
?: instanceInfo?.description?.notBlank()

View File

@ -80,12 +80,12 @@ class DlgListMember(
who.avatar_static,
who.avatar
)
val userNameInvalidator = NetworkEmojiInvalidator(activity.handler, tvUserName)
val name = who.decodeDisplayName(activity)
tvUserName.text = name
userNameInvalidator.register(name)
tvUserAcct.text = targetUserFullAcct.pretty
val name = who.decodeDisplayName(activity)
val userNameInvalidator = NetworkEmojiInvalidator(activity.handler, tvUserName)
userNameInvalidator.text = name
setListOwner(listOwner)
dialog = Dialog(activity).apply {

View File

@ -129,13 +129,11 @@ fun ItemViewHolder.makeEnqueteChoiceView(
val b = AppCompatTextView(activity)
b.layoutParams = lp
b.padding = (activity.density * 3f + 0.5f).toInt()
b.text = text
val invalidator = NetworkEmojiInvalidator(activity.handler, b)
extraInvalidatorList.add(invalidator)
invalidator.register(text)
b.padding = (activity.density * 3f + 0.5f).toInt()
invalidator.text = text
val ratio = when (enquete.pollType) {
TootPollsType.Mastodon -> {
@ -173,10 +171,11 @@ fun ItemViewHolder.makeEnqueteChoiceView(
val b = CheckBox(activity)
b.layoutParams = lp
b.isAllCaps = false
b.text = text
val invalidator = NetworkEmojiInvalidator(activity.handler, b)
extraInvalidatorList.add(invalidator)
invalidator.register(text)
invalidator.text = text
if (!canVote) {
b.isEnabledAlpha = false
} else {
@ -190,10 +189,11 @@ fun ItemViewHolder.makeEnqueteChoiceView(
val b = AppCompatButton(activity)
b.layoutParams = lp
b.isAllCaps = false
b.text = text
val invalidator = NetworkEmojiInvalidator(activity.handler, b)
extraInvalidatorList.add(invalidator)
invalidator.register(text)
invalidator.text = text
if (!canVote) {
b.isEnabled = false
} else {

View File

@ -6,6 +6,7 @@ import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.column.isConversation
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.pref.PrefS
import jp.juggler.subwaytooter.span.emojiSizeMode
import jp.juggler.subwaytooter.table.daoMediaShown
import jp.juggler.subwaytooter.util.DecodeOptions
import jp.juggler.subwaytooter.util.HTMLDecoder
@ -68,7 +69,8 @@ fun ItemViewHolder.showPreviewCard(status: TootStatus) {
val text = DecodeOptions(
activity, accessInfo,
forceHtml = true,
authorDomain = status.account
authorDomain = status.account,
emojiSizeMode = accessInfo.emojiSizeMode(),
).decodeHTML(sb.toString())
if (text.isNotEmpty()) {

View File

@ -18,6 +18,7 @@ import jp.juggler.subwaytooter.api.entity.TootReaction
import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.pref.PrefI
import jp.juggler.subwaytooter.span.emojiSizeMode
import jp.juggler.subwaytooter.util.DecodeOptions
import jp.juggler.subwaytooter.util.NetworkEmojiInvalidator
import jp.juggler.subwaytooter.util.minWidthCompat
@ -74,6 +75,7 @@ fun ItemViewHolder.makeReactionsView(status: TootStatus) {
decodeEmoji = true,
enlargeEmoji = imageScale,
enlargeCustomEmoji = imageScale,
emojiSizeMode = accessInfo.emojiSizeMode(),
)
reactionSet?.forEachIndexed { index, reaction ->
@ -132,15 +134,12 @@ fun ItemViewHolder.makeReactionsView(status: TootStatus) {
// カスタム絵文字の場合、アニメーション等のコールバックを処理する必要がある
val invalidator = NetworkEmojiInvalidator(act.handler, this)
extraInvalidatorList.add(invalidator)
try {
val ssb =
reaction.toSpannableStringBuilder(options, status)
.also { it.append(" ${reaction.count}") }
text = ssb
invalidator.register(ssb)
invalidator.text = try {
reaction.toSpannableStringBuilder(options, status)
.also { it.append(" ${reaction.count}") }
} catch (ex: Throwable) {
log.e(ex, "can't decode reaction emoji.")
text = "${reaction.name} ${reaction.count}"
"${reaction.name} ${reaction.count}"
}
}
box.addView(b)

View File

@ -25,6 +25,7 @@ import jp.juggler.subwaytooter.drawable.PreviewCardBorder
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.pref.PrefI
import jp.juggler.subwaytooter.span.MyClickableSpan
import jp.juggler.subwaytooter.span.emojiSizeMode
import jp.juggler.subwaytooter.table.*
import jp.juggler.subwaytooter.util.DecodeOptions
import jp.juggler.subwaytooter.view.MyNetworkImageView
@ -268,14 +269,12 @@ fun ItemViewHolder.showAccount(whoRef: TootAccountRef) {
accessInfo.supplyBaseUrl(who.avatar)
)
tvFollowerName.text = whoRef.decoded_display_name
followInvalidator.register(whoRef.decoded_display_name)
followInvalidator.text = whoRef.decoded_display_name
setAcct(tvFollowerAcct, accessInfo, who)
who.setAccountExtra(
accessInfo,
tvLastStatusAt,
lastActiveInvalidator,
suggestionSource = if (column.type == ColumnType.FOLLOW_SUGGESTION) {
SuggestionSource.get(accessInfo.db_id, who.acct)
@ -356,7 +355,8 @@ fun ItemViewHolder.showBoost(
accessInfo,
decodeEmoji = true,
enlargeEmoji = 1.5f,
enlargeCustomEmoji = 1.5f
enlargeCustomEmoji = 1.5f,
emojiSizeMode = accessInfo.emojiSizeMode(),
)
val ssb = reaction.toSpannableStringBuilder(options, boostStatus)
ssb.append(" ")
@ -364,15 +364,11 @@ fun ItemViewHolder.showBoost(
who.decodeDisplayNameCached(activity)
.intoStringResource(activity, stringId)
)
val text: Spannable =ssb
tvBoosted.text = text
boostInvalidator.register(text)
boostInvalidator.text = ssb
} else {
val text: Spannable =
boostInvalidator.text =
who.decodeDisplayNameCached(activity)
.intoStringResource(activity, stringId)
tvBoosted.text = text
boostInvalidator.register(text)
.intoStringResource(activity, stringId)
}
}
@ -501,8 +497,7 @@ fun ItemViewHolder.showReply(
)
}
tvReply.text = text
replyInvalidator.register(text)
replyInvalidator.text = text
}
fun ItemViewHolder.showReply(replyer: TootAccount?, reply: TootStatus, iconId: Int, stringId: Int) {
@ -738,8 +733,8 @@ fun ItemViewHolder.showScheduled(item: TootScheduled) {
setAcct(tvAcct, accessInfo, who)
tvName.text = whoRef.decoded_display_name
nameInvalidator.register(whoRef.decoded_display_name)
nameInvalidator.text = whoRef.decoded_display_name
ivAvatar.setImageUrl(
calcIconRound(ivAvatar.layoutParams),
accessInfo.supplyBaseUrl(who.avatar_static),
@ -750,8 +745,7 @@ fun ItemViewHolder.showScheduled(item: TootScheduled) {
tvMentions.visibility = View.GONE
tvContent.text = content
contentInvalidator.register(content)
contentInvalidator.text = content
tvContent.minLines = -1
@ -760,8 +754,7 @@ fun ItemViewHolder.showScheduled(item: TootScheduled) {
decodedSpoilerText.isNotEmpty() -> {
// 元データに含まれるContent Warning を使う
llContentWarning.visibility = View.VISIBLE
tvContentWarning.text = decodedSpoilerText
spoilerInvalidator.register(decodedSpoilerText)
spoilerInvalidator.text = decodedSpoilerText
val cwShown = daoContentWarning.isShown(item.uri, accessInfo.expandCw)
setContentVisibility(cwShown)
}

View File

@ -104,8 +104,8 @@ fun ItemViewHolder.showStatus(
setAcct(tvAcct, accessInfo, who)
tvName.text = whoRef.decoded_display_name
nameInvalidator.register(whoRef.decoded_display_name)
nameInvalidator.text = whoRef.decoded_display_name
ivAvatar.setImageUrl(
calcIconRound(ivAvatar.layoutParams),
accessInfo.supplyBaseUrl(who.avatar_static),
@ -147,8 +147,7 @@ fun ItemViewHolder.showStatus(
tvMentions.textOrGone = status.decoded_mentions
tvContent.text = modifiedContent
contentInvalidator.register(modifiedContent)
contentInvalidator.text = modifiedContent
activity.checkAutoCW(status, modifiedContent)
val r = status.auto_cw
@ -183,21 +182,20 @@ private fun ItemViewHolder.showPoll(status: TootStatus): Spannable? {
private fun ItemViewHolder.showSpoilerTextAndContent(status: TootStatus) {
val r = status.auto_cw
val decodedSpoilerText = status.decoded_spoiler_text
val autoCwText = r?.decodedSpoilerText
when {
decodedSpoilerText.isNotEmpty() -> {
// 元データに含まれるContent Warning を使う
llContentWarning.visibility = View.VISIBLE
tvContentWarning.text = status.decoded_spoiler_text
spoilerInvalidator.register(status.decoded_spoiler_text)
spoilerInvalidator.text = status.decoded_spoiler_text
val cwShown = daoContentWarning.isShown(status, accessInfo.expandCw)
setContentVisibility(cwShown)
}
r?.decodedSpoilerText != null -> {
autoCwText != null -> {
// 自動CW
llContentWarning.visibility = View.VISIBLE
tvContentWarning.text = r.decodedSpoilerText
spoilerInvalidator.register(r.decodedSpoilerText)
spoilerInvalidator.text = autoCwText
val cwShown = daoContentWarning.isShown(status, accessInfo.expandCw)
setContentVisibility(cwShown)
}
@ -221,10 +219,11 @@ fun ItemViewHolder.setContentVisibility(shown: Boolean) {
statusShowing?.let { status ->
val r = status.auto_cw
tvContent.minLines = r?.originalLineCount ?: -1
if (r?.decodedSpoilerText != null) {
val autoCwText = r?.decodedSpoilerText
if (autoCwText != null) {
// 自動CWの場合はContentWarningのテキストを切り替える
tvContentWarning.text =
if (shown) activity.getString(R.string.auto_cw_prefix) else r.decodedSpoilerText
spoilerInvalidator.text =
if (shown) activity.getString(R.string.auto_cw_prefix) else autoCwText
}
}
}

View File

@ -3,6 +3,7 @@ package jp.juggler.subwaytooter.span
interface AnimatableSpanInvalidator {
val timeFromStart: Long
fun delayInvalidate(delay: Long)
fun requestLayout()
}
interface AnimatableSpan {

View File

@ -6,29 +6,51 @@ import android.graphics.Rect
import android.graphics.RectF
import android.graphics.drawable.Drawable
import android.text.style.ReplacementSpan
import androidx.annotation.DrawableRes
import androidx.annotation.IntRange
import androidx.collection.LruCache
import androidx.core.content.ContextCompat
import jp.juggler.apng.ApngFrames
import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.entity.TootInstance
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.pref.lazyContext
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.log.LogCategory
import java.lang.ref.WeakReference
import kotlin.math.min
class NetworkEmojiSpan internal constructor(
enum class EmojiSizeMode {
Square,
Wide,
}
fun SavedAccount?.emojiSizeMode(): EmojiSizeMode {
val ti = this?.let { TootInstance.getCached(it) }
return when {
ti == null -> EmojiSizeMode.Square
ti.isMisskey || !ti.fedibirdCapabilities.isNullOrEmpty() -> EmojiSizeMode.Wide
else -> EmojiSizeMode.Square
}
}
class NetworkEmojiSpan constructor(
private val url: String,
private val sizeMode: EmojiSizeMode,
private val scale: Float = 1f,
@DrawableRes private val errorDrawableId: Int = R.drawable.outline_broken_image_24,
private val errorDrawableId: Int = R.drawable.outline_broken_image_24,
) : ReplacementSpan(), AnimatableSpan {
companion object {
internal val log = LogCategory("NetworkEmojiSpan")
private const val scaleRatio = 1.14f
private const val descentRatio = 0.211f
// 最大幅
var maxEmojiWidth = Float.MAX_VALUE
// ImageWidthCache
val imageAspectCache = LruCache<String, Float>(1024)
}
private val mPaint = Paint().apply { isFilterBitmap = true }
@ -44,6 +66,67 @@ class NetworkEmojiSpan internal constructor(
private var errorDrawableCache: Drawable? = null
private var lastWidth: Float? = null
private var transY = 0f
private var baseHeight = 0f
/**
* lastAspect に基づいて rectDst transY を更新する
*/
private fun updateRect(aspectArg: Float? = null, textSize: Float, baseline: Float) {
// テキストサイズをスケーリングした基本高さ
this.baseHeight = textSize * scaleRatio * scale
// ベースラインから上下方向にずらすオフセット
val cDescent = baseHeight * descentRatio
this.transY = baseline - baseHeight + cDescent
val aspect = when(aspectArg){
null ->{
imageAspectCache[url] ?: 1f
}
else->{
imageAspectCache.put(url,aspectArg)
aspectArg
}
}.takeIf { it>0f }?:1f
when {
// 横長画像で、それを許可するモード
aspect > 1f && sizeMode == EmojiSizeMode.Wide -> {
// 絵文字のアスペクト比から描画範囲の幅と高さを決める
val dstWidth = min(maxEmojiWidth, aspect * baseHeight)
val dstHeight = dstWidth / aspect
val dstX = 0f
val dstY = (baseHeight - dstHeight) / 2f
rectDst.set(dstX, dstY, dstX + dstWidth, dstY + dstHeight)
}
else -> {
// 絵文字のアスペクト比から描画範囲の幅と高さを決める
val dstWidth: Float
val dstHeight: Float
if (aspect >= 1f) {
dstWidth = baseHeight
dstHeight = baseHeight / aspect
} else {
dstHeight = baseHeight
dstWidth = baseHeight * aspect
}
val dstX = (baseHeight - dstWidth) / 2f
val dstY = (baseHeight - dstHeight) / 2f
rectDst.set(dstX, dstY, dstX + dstWidth, dstY + dstHeight)
}
}
// 出力サイズが変化したならrequestLayout
val newWidth = rectDst.width()
if (lastWidth != null && lastWidth != newWidth) {
log.i("updateRect: width changed. $lastWidth$newWidth")
invalidateCallback?.requestLayout()
}
lastWidth = newWidth
}
override fun setInvalidateCallback(
drawTargetTag: Any,
invalidateCallback: AnimatableSpanInvalidator,
@ -59,16 +142,17 @@ class NetworkEmojiSpan internal constructor(
@IntRange(from = 0) end: Int,
fm: Paint.FontMetricsInt?,
): Int {
val size = (paint.textSize * scaleRatio * scale + 0.5f).toInt()
updateRect(aspectArg = null, paint.textSize, baseline = 0f)
val height = (baseHeight + 0.5f).toInt()
if (fm != null) {
val cDescent = (0.5f + size * descentRatio).toInt()
val cAscent = cDescent - size
val cDescent = (0.5f + height * descentRatio).toInt()
val cAscent = cDescent - height
if (fm.ascent > cAscent) fm.ascent = cAscent
if (fm.top > cAscent) fm.top = cAscent
if (fm.descent < cDescent) fm.descent = cDescent
if (fm.bottom < cDescent) fm.bottom = cDescent
}
return size
return (rectDst.width() + 0.5f).toInt()
}
override fun draw(
@ -123,28 +207,11 @@ class NetworkEmojiSpan internal constructor(
return false
}
rectSrc.set(0, 0, srcWidth, srcHeight)
// 絵文字の正方形のサイズ
val dstSize = textPaint.textSize * scaleRatio * scale
// ベースラインから上下方向にずらすオフセット
val cDescent = dstSize * descentRatio
val transY = baseline - dstSize + cDescent
// 絵文字のアスペクト比から描画範囲の幅と高さを決める
val dstWidth: Float
val dstHeight: Float
val aspectSrc = srcWidth.toFloat() / srcHeight.toFloat()
if (aspectSrc >= 1f) {
dstWidth = dstSize
dstHeight = dstSize / aspectSrc
} else {
dstHeight = dstSize
dstWidth = dstSize * aspectSrc
}
val dstX = (dstSize - dstWidth) / 2f
val dstY = (dstSize - dstHeight) / 2f
rectDst.set(dstX, dstY, dstX + dstWidth, dstY + dstHeight)
updateRect(
aspectArg = srcWidth.toFloat() / srcHeight.toFloat(),
textPaint.textSize,
baseline.toFloat()
)
canvas.save()
try {
@ -184,38 +251,23 @@ class NetworkEmojiSpan internal constructor(
?.also { errorDrawableCache = it }
drawable ?: return
val srcWidth = drawable.intrinsicWidth
val srcHeight = drawable.intrinsicHeight
val srcWidth = drawable.intrinsicWidth.toFloat()
val srcHeight = drawable.intrinsicHeight.toFloat()
// 絵文字の正方形のサイズ
val dstSize = textPaint.textSize * scaleRatio * scale
updateRect(
aspectArg = srcWidth / srcHeight,
textSize = textPaint.textSize,
baseline = baseline.toFloat()
)
// ベースラインから上下方向にずらすオフセット
val cDescent = dstSize * descentRatio
val transY = baseline - dstSize + cDescent
// 絵文字のアスペクト比から描画範囲の幅と高さを決める
val dstWidth: Float
val dstHeight: Float
val aspectSrc = srcWidth.toFloat() / srcHeight.toFloat()
if (aspectSrc >= 1f) {
dstWidth = dstSize
dstHeight = dstSize / aspectSrc
} else {
dstHeight = dstSize
dstWidth = dstSize * aspectSrc
}
val dstX = (dstSize - dstWidth) / 2f
val dstY = (dstSize - dstHeight) / 2f
// rectDst.set(dstX, dstY, dstX + dstWidth, dstY + dstHeight)
canvas.save()
try {
canvas.translate(x, transY)
drawable.setBounds(
dstX.toInt(),
dstY.toInt(),
(dstX + dstWidth).toInt(),
(dstY + dstHeight).toInt()
rectDst.left.toInt(),
rectDst.top.toInt(),
rectDst.right.plus(0.5f).toInt(),
rectDst.bottom.plus(0.5f).toInt(),
)
drawable.draw(canvas)
} catch (ex: Throwable) {

View File

@ -319,9 +319,11 @@ class CustomEmojiCache(
private fun decodeAPNG(data: ByteArray, url: String): ApngFrames? {
val errors = ArrayList<Throwable>()
val maxSize = 256
try {
// APNGをデコード AWebPも
val x = ApngFrames.parse(64) { ByteArrayInputStream(data) }
val x = ApngFrames.parse(maxSize) { ByteArrayInputStream(data) }
if (x != null) return x
error("ApngFrames.parse returns null.")
} catch (ex: Throwable) {
@ -331,7 +333,7 @@ class CustomEmojiCache(
// 通常のビットマップでのロードを試みる
try {
val b = decodeBitmap(data, 128)
val b = decodeBitmap(data, maxSize)
if (b != null) return ApngFrames(b)
error("decodeBitmap returns null.")
} catch (ex: Throwable) {
@ -341,7 +343,7 @@ class CustomEmojiCache(
// SVGのロードを試みる
try {
val b = decodeSVG(url, data, 128.toFloat())
val b = decodeSVG(url, data, maxSize.toFloat())
if (b != null) return ApngFrames(b)
error("decodeSVG returns null.")
} catch (ex: Throwable) {

View File

@ -9,6 +9,7 @@ import jp.juggler.subwaytooter.api.entity.NicoProfileEmoji
import jp.juggler.subwaytooter.api.entity.TootAttachmentLike
import jp.juggler.subwaytooter.api.entity.TootMention
import jp.juggler.subwaytooter.emoji.CustomEmoji
import jp.juggler.subwaytooter.span.EmojiSizeMode
import jp.juggler.subwaytooter.table.HighlightWord
import jp.juggler.util.data.WordTrieTree
import org.jetbrains.anko.collections.forEachReversedByIndex
@ -33,6 +34,7 @@ class DecodeOptions(
// Account.note などmentionsがない状況でメンションリンクをfull acct化するにはアカウント等からapDomainを補う必要がある
// MFMはメンションのホスト名を補うのに閲覧者ではなく投稿作者のホスト名を必要とする
var authorDomain: HostAndDomain? = null,
var emojiSizeMode: EmojiSizeMode = EmojiSizeMode.Square,
) {
internal fun isMediaAttachment(url: String?): Boolean =

View File

@ -5,7 +5,6 @@ import android.text.SpannableStringBuilder
import android.text.Spanned
import android.util.SparseBooleanArray
import androidx.annotation.DrawableRes
import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.emoji.CustomEmoji
import jp.juggler.subwaytooter.emoji.EmojiMap
@ -143,7 +142,11 @@ object EmojiDecoder {
sb.append(text)
val end = sb.length
sb.setSpan(
NetworkEmojiSpan(url, scale = options.enlargeCustomEmoji),
NetworkEmojiSpan(
url,
scale = options.enlargeCustomEmoji,
sizeMode = options.emojiSizeMode
),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
@ -396,7 +399,7 @@ object EmojiDecoder {
else -> {
// 存在確認せずに絵文字プロキシのURLを返す
// 閲覧先サーバの絵文字を探す
return "https://${apiHostAscii}/emoji/${ cols.elementAtOrNull(0)}.webp"
return "https://${apiHostAscii}/emoji/${cols.elementAtOrNull(0)}.webp"
}
}
return null

View File

@ -528,7 +528,11 @@ object HTMLDecoder {
append(alt)
val end = length
setSpan(
NetworkEmojiSpan(url, scale = options.enlargeCustomEmoji),
NetworkEmojiSpan(
url,
scale = options.enlargeCustomEmoji,
sizeMode = options.emojiSizeMode
),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE

View File

@ -1,6 +1,8 @@
package jp.juggler.subwaytooter.util
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.span.EmojiSizeMode
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.data.findOrNull
import jp.juggler.util.data.groupEx
@ -69,7 +71,7 @@ fun LinkHelper.matchHost(src: TootAccount) =
apiHost == src.apiHost || apDomain == src.apDomain ||
apDomain == src.apiHost || apiHost == src.apDomain
fun LinkHelper.matchHost(srcApiHost:Host,srcApDomain:Host) =
fun LinkHelper.matchHost(srcApiHost: Host, srcApDomain: Host) =
apiHost == srcApiHost ||
apDomain == srcApDomain ||
apDomain == srcApiHost ||

View File

@ -3,16 +3,24 @@ package jp.juggler.subwaytooter.util
import android.os.Handler
import android.os.SystemClock
import android.text.Spannable
import android.view.View
import java.util.ArrayList
import android.widget.TextView
import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.span.AnimatableSpan
import jp.juggler.subwaytooter.span.AnimatableSpanInvalidator
import java.lang.ref.WeakReference
class NetworkEmojiInvalidator(internal val handler: Handler, internal val view: View) : Runnable, AnimatableSpanInvalidator {
class NetworkEmojiInvalidator(
internal val handler: Handler,
internal val view: TextView,
// val parent:View? = null,
) : AnimatableSpanInvalidator {
var text: CharSequence
get() = view.text
set(value) {
if (value is Spannable) register(value)
view.text = value
}
private val drawTargetList = ArrayList<WeakReference<Any>>()
@ -34,6 +42,43 @@ class NetworkEmojiInvalidator(internal val handler: Handler, internal val view:
return now - tStart
}
// Handler経由で遅延実行される
private val runnableInvalidate = object : Runnable {
override fun run() {
// 後から来たのをこのタイミングで重複排除する
handler.removeCallbacks(this)
if (view.isAttachedToWindow) {
view.postInvalidateOnAnimation()
}
}
}
// Handler経由で遅延実行される
private val runnableRequestLayout = object : Runnable {
override fun run() {
// 後から来たのをこのタイミングで重複排除する
handler.removeCallbacks(this)
if (view.isAttachedToWindow) {
view.requestLayout()
}
}
}
// 絵文字スパンを描画した直後に呼ばれる
// (絵文字が多いと描画の度に大量に呼び出される)
override fun delayInvalidate(delay: Long) {
// あとから来たのをconflateしたいので、このタイミングでは removeCallbacks しない
handler.postDelayed(
runnableInvalidate,
if (delay < 10L) 10L else if (delay > 711L) 711L else delay
)
}
override fun requestLayout() {
// あとから来たのをconflateしたいので、このタイミングでは removeCallbacks しない
handler.postDelayed(runnableRequestLayout, 10L)
}
fun clear() {
for (o in drawTargetList) {
App1.custom_emoji_cache.cancelRequest(o)
@ -53,18 +98,4 @@ class NetworkEmojiInvalidator(internal val handler: Handler, internal val view:
}
}
}
// 絵文字スパンを描画した直後に呼ばれる
// (絵文字が多いと描画の度に大量に呼び出される)
override fun delayInvalidate(delay: Long) {
handler.postDelayed(this, if (delay < 10L) 10L else if (delay > 711L) 711L else delay)
}
// Handler経由で遅延実行される
override fun run() {
handler.removeCallbacks(this)
if (view.isAttachedToWindow) {
view.postInvalidateOnAnimation()
}
}
}

View File

@ -117,7 +117,7 @@ internal class PopupAutoCompleteAcct(
v.setTextColor(activity.attrColor(android.R.attr.textColorPrimary))
v.text = acct
if (acct is Spannable) {
NetworkEmojiInvalidator(handler, v).register(acct)
NetworkEmojiInvalidator(handler, v).text = acct
}
v.setOnClickListener { handleItemClick(et, selStart, selEnd, acct) }
llItems.addView(v)

View File

@ -7,6 +7,7 @@ import jp.juggler.subwaytooter.ActText
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.span.emojiSizeMode
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.data.notEmpty
import java.util.*
@ -72,6 +73,7 @@ object TootTextEncoder {
accessInfo,
mentions = status.mentions,
authorDomain = status.account,
emojiSizeMode = accessInfo.emojiSizeMode(),
).decodeHTML(status.content)
)
@ -103,6 +105,7 @@ object TootTextEncoder {
accessInfo,
mentions = status.mentions,
authorDomain = status.account,
emojiSizeMode = accessInfo.emojiSizeMode(),
).decodeHTML(status.content)
)
@ -271,6 +274,7 @@ object TootTextEncoder {
context,
accessInfo,
authorDomain = who,
emojiSizeMode = accessInfo.emojiSizeMode(),
).decodeHTML(who.note)
)