アプリ設定に絵文字セクションと設定項目をいくつか追加。絵文字のアスペクト比をDBに保存する。

This commit is contained in:
tateisu 2023-02-26 08:15:57 +09:00
parent d83efef4d9
commit aefcc7c798
27 changed files with 312 additions and 86 deletions

View File

@ -236,6 +236,9 @@ class ApngFrames private constructor(
} }
} }
val aspect:Float?
get() = if( width<=0 || height<=0) null else width.toFloat().div(height)
constructor(bitmap: Bitmap) : this() { constructor(bitmap: Bitmap) : this() {
defaultImage = bitmap defaultImage = bitmap
} }

View File

@ -38,6 +38,7 @@ import jp.juggler.subwaytooter.span.MyClickableSpan
import jp.juggler.subwaytooter.span.MyClickableSpanHandler import jp.juggler.subwaytooter.span.MyClickableSpanHandler
import jp.juggler.subwaytooter.table.daoSavedAccount import jp.juggler.subwaytooter.table.daoSavedAccount
import jp.juggler.subwaytooter.util.* import jp.juggler.subwaytooter.util.*
import jp.juggler.subwaytooter.util.DecodeOptions.Companion.reloadEmojiScale
import jp.juggler.subwaytooter.view.MyDrawerLayout import jp.juggler.subwaytooter.view.MyDrawerLayout
import jp.juggler.subwaytooter.view.MyEditText import jp.juggler.subwaytooter.view.MyEditText
import jp.juggler.subwaytooter.view.MyNetworkImageView import jp.juggler.subwaytooter.view.MyNetworkImageView
@ -370,6 +371,7 @@ class ActMain : AppCompatActivity(),
acctPadLr = (0.5f + 4f * density).toInt() acctPadLr = (0.5f + 4f * density).toInt()
reloadTextSize() reloadTextSize()
reloadEmojiScale()
initUI() initUI()
@ -455,6 +457,7 @@ class ActMain : AppCompatActivity(),
super.onStart() super.onStart()
galaxyBackgroundWorkaround() galaxyBackgroundWorkaround()
benchmark("onStart total") { benchmark("onStart total") {
reloadEmojiScale()
benchmark("reload color") { reloadColors() } benchmark("reload color") { reloadColors() }
benchmark("reload timezone") { reloadTimeZone() } benchmark("reload timezone") { reloadTimeZone() }

View File

@ -120,8 +120,8 @@ fun ActMain.reactionAdd(
activity, activity,
accessInfo, accessInfo,
decodeEmoji = true, decodeEmoji = true,
enlargeEmoji = 1.5f, enlargeEmoji = DecodeOptions.emojiScaleReaction,
enlargeCustomEmoji = 1.5f, enlargeCustomEmoji = DecodeOptions.emojiScaleReaction,
emojiSizeMode = accessInfo.emojiSizeMode(), emojiSizeMode = accessInfo.emojiSizeMode(),
) )
val emojiSpan = TootReaction.toSpannableStringBuilder(options, code, urlArg) val emojiSpan = TootReaction.toSpannableStringBuilder(options, code, urlArg)
@ -219,8 +219,8 @@ fun ActMain.reactionRemove(
activity, activity,
accessInfo, accessInfo,
decodeEmoji = true, decodeEmoji = true,
enlargeEmoji = 1.5f, enlargeEmoji = DecodeOptions.emojiScaleReaction,
enlargeCustomEmoji = 1.5f, enlargeCustomEmoji = DecodeOptions.emojiScaleReaction,
emojiSizeMode = accessInfo.emojiSizeMode(), emojiSizeMode = accessInfo.emojiSizeMode(),
) )
val emojiSpan = reaction.toSpannableStringBuilder(options, status) val emojiSpan = reaction.toSpannableStringBuilder(options, status)
@ -330,8 +330,8 @@ private fun ActMain.reactionWithoutUi(
activity, activity,
accessInfo, accessInfo,
decodeEmoji = true, decodeEmoji = true,
enlargeEmoji = 1.5f, enlargeEmoji = DecodeOptions.emojiScaleReaction,
enlargeCustomEmoji = 1.5f, enlargeCustomEmoji = DecodeOptions.emojiScaleReaction,
emojiSizeMode = accessInfo.emojiSizeMode(), emojiSizeMode = accessInfo.emojiSizeMode(),
) )
val emojiSpan = TootReaction.toSpannableStringBuilder(options, reactionCode, reactionImage) val emojiSpan = TootReaction.toSpannableStringBuilder(options, reactionCode, reactionImage)

View File

@ -14,6 +14,7 @@ import jp.juggler.subwaytooter.util.emojiSizeMode
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.daoUserRelation import jp.juggler.subwaytooter.table.daoUserRelation
import jp.juggler.subwaytooter.util.DecodeOptions import jp.juggler.subwaytooter.util.DecodeOptions
import jp.juggler.subwaytooter.util.DecodeOptions.Companion.emojiScaleUserName
import jp.juggler.subwaytooter.util.LinkHelper import jp.juggler.subwaytooter.util.LinkHelper
import jp.juggler.subwaytooter.util.NetworkEmojiInvalidator import jp.juggler.subwaytooter.util.NetworkEmojiInvalidator
import jp.juggler.subwaytooter.util.matchHost import jp.juggler.subwaytooter.util.matchHost
@ -168,6 +169,8 @@ open class TootAccount(
emojiMapProfile = profile_emojis, emojiMapProfile = profile_emojis,
emojiMapCustom = custom_emojis, emojiMapCustom = custom_emojis,
authorDomain = this, authorDomain = this,
enlargeCustomEmoji = emojiScaleUserName,
enlargeEmoji = emojiScaleUserName,
).decodeEmoji(sv) ).decodeEmoji(sv)
} }

View File

@ -4,6 +4,8 @@ import android.text.Spannable
import jp.juggler.subwaytooter.api.TootAccountMap import jp.juggler.subwaytooter.api.TootAccountMap
import jp.juggler.subwaytooter.api.TootParser import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.util.DecodeOptions import jp.juggler.subwaytooter.util.DecodeOptions
import jp.juggler.subwaytooter.util.DecodeOptions.Companion.emojiScaleMastodon
import jp.juggler.subwaytooter.util.DecodeOptions.Companion.emojiScaleMisskey
class TootAccountRef private constructor( class TootAccountRef private constructor(
val mapId: Int, val mapId: Int,
@ -50,6 +52,8 @@ class TootAccountRef private constructor(
unwrapEmojiImageTag = true, unwrapEmojiImageTag = true,
authorDomain = account, authorDomain = account,
emojiSizeMode = parser.emojiSizeMode, emojiSizeMode = parser.emojiSizeMode,
enlargeEmoji = if(parser.linkHelper.isMisskey) emojiScaleMisskey else emojiScaleMastodon,
enlargeCustomEmoji = if(parser.linkHelper.isMisskey) emojiScaleMisskey else emojiScaleMastodon,
).decodeHTML(account.note), ).decodeHTML(account.note),
) )
} }

View File

@ -343,7 +343,6 @@ val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_sett
} }
sw(PrefB.bpDontRemoveDeletedToot, R.string.dont_remove_deleted_toot_from_timeline) sw(PrefB.bpDontRemoveDeletedToot, R.string.dont_remove_deleted_toot_from_timeline)
sw(PrefB.bpCustomEmojiSeparatorZwsp, R.string.custom_emoji_separator_zwsp)
sw(PrefB.bpShowTranslateButton, R.string.show_translate_button) sw(PrefB.bpShowTranslateButton, R.string.show_translate_button)
item( item(
@ -464,10 +463,6 @@ val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_sett
R.string.warn_hashtag_ascii_and_non_ascii R.string.warn_hashtag_ascii_and_non_ascii
) )
sw(
PrefB.bpEmojiPickerCloseOnSelected,
R.string.close_emoji_picker_when_selected
)
sw(PrefB.bpIgnoreTextInSharedMedia, R.string.ignore_text_in_shared_media) sw(PrefB.bpIgnoreTextInSharedMedia, R.string.ignore_text_in_shared_media)
} }
@ -525,6 +520,58 @@ val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_sett
sw(PrefB.bpDisableEmojiAnimation, R.string.disable_custom_emoji_animation) sw(PrefB.bpDisableEmojiAnimation, R.string.disable_custom_emoji_animation)
} }
section(R.string.emoji){
sw(PrefB.bpUseTwemoji, R.string.use_twemoji_emoji)
sw(PrefB.bpEmojioneShortcode, R.string.emojione_shortcode_support) {
desc = R.string.emojione_shortcode_support_desc
}
sw(PrefB.bpEmojiPickerCategoryOther, R.string.show_emoji_picker_other_category)
sw(
PrefB.bpEmojiPickerCloseOnSelected,
R.string.close_emoji_picker_when_selected
)
sw(PrefB.bpCustomEmojiSeparatorZwsp, R.string.custom_emoji_separator_zwsp)
text(
PrefS.spEmojiSizeMastodon,
R.string.emoji_size_mastodon,
InputTypeEx.number
)
text(
PrefS.spEmojiSizeMisskey,
R.string.emoji_size_misskey,
InputTypeEx.number
)
text(
PrefS.spEmojiSizeReaction,
R.string.emoji_size_reaction,
InputTypeEx.number
)
text(
PrefS.spEmojiSizeUserName,
R.string.emoji_size_user_name,
InputTypeEx.number
)
spinnerSimple(
PrefI.ipEmojiWideMode,
R.string.emoji_wide_mode,
R.string.auto,
R.string.enabled,
R.string.disabled,
)
text(
PrefS.spEmojiPixels,
R.string.emoji_texture_pixels,
InputTypeEx.number
)
}
section(R.string.appearance) { section(R.string.appearance) {
sw(PrefB.bpSimpleList, R.string.simple_list) sw(PrefB.bpSimpleList, R.string.simple_list)
sw(PrefB.bpShowFollowButtonInButtonBar, R.string.show_follow_button_in_button_bar) sw(PrefB.bpShowFollowButtonInButtonBar, R.string.show_follow_button_in_button_bar)
@ -789,10 +836,6 @@ val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_sett
) )
sw(PrefB.bpShowBookmarkButton, R.string.show_bookmark_button) sw(PrefB.bpShowBookmarkButton, R.string.show_bookmark_button)
sw(PrefB.bpHideFollowCount, R.string.hide_followers_count) sw(PrefB.bpHideFollowCount, R.string.hide_followers_count)
sw(PrefB.bpEmojioneShortcode, R.string.emojione_shortcode_support) {
desc = R.string.emojione_shortcode_support_desc
}
sw(PrefB.bpUseTwemoji, R.string.use_twemoji_emoji)
sw(PrefB.bpKeepReactionSpace, R.string.keep_reaction_space) sw(PrefB.bpKeepReactionSpace, R.string.keep_reaction_space)
@ -1032,7 +1075,6 @@ val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_sett
sw(PrefB.bpEnableDeprecatedSomething,R.string.enable_deprecated_something) sw(PrefB.bpEnableDeprecatedSomething,R.string.enable_deprecated_something)
sw(PrefB.bpEmojiPickerCategoryOther, R.string.show_emoji_picker_other_category)
action(R.string.drawable_list) { action(R.string.drawable_list) {
action = { startActivity(Intent(this, ActDrawableList::class.java)) } action = { startActivity(Intent(this, ActDrawableList::class.java)) }
} }

View File

@ -61,8 +61,8 @@ fun ColumnViewHolder.updateReactionQueryView() {
act, act,
column.accessInfo, column.accessInfo,
decodeEmoji = true, decodeEmoji = true,
enlargeEmoji = 1.5f, enlargeEmoji = DecodeOptions.emojiScaleReaction,
enlargeCustomEmoji = 1.5f, enlargeCustomEmoji = DecodeOptions.emojiScaleReaction,
emojiSizeMode = column.accessInfo.emojiSizeMode(), emojiSizeMode = column.accessInfo.emojiSizeMode(),
) )

View File

@ -624,6 +624,8 @@ internal class ViewHolderHeaderProfile(
emojiMapProfile = who.profile_emojis, emojiMapProfile = who.profile_emojis,
authorDomain = who, authorDomain = who,
emojiSizeMode = accessInfo.emojiSizeMode(), emojiSizeMode = accessInfo.emojiSizeMode(),
enlargeCustomEmoji = DecodeOptions.emojiScaleUserName,
enlargeEmoji = DecodeOptions.emojiScaleUserName,
) )
val nameTypeface = ActMain.timelineFontBold val nameTypeface = ActMain.timelineFontBold

View File

@ -290,7 +290,6 @@ private class EmojiPicker(
item.customEmoji.url item.customEmoji.url
}, },
initialAspect = item.customEmoji.aspect, initialAspect = item.customEmoji.aspect,
defaultWidth = gridSize,
defaultHeight = gridSize, defaultHeight = gridSize,
) )
} }

View File

@ -10,6 +10,7 @@ import com.google.android.flexbox.FlexWrap
import com.google.android.flexbox.FlexboxLayout import com.google.android.flexbox.FlexboxLayout
import com.google.android.flexbox.JustifyContent import com.google.android.flexbox.JustifyContent
import jp.juggler.subwaytooter.ActMain import jp.juggler.subwaytooter.ActMain
import jp.juggler.subwaytooter.ActMain.Companion.boostButtonSize
import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.action.reactionAdd import jp.juggler.subwaytooter.action.reactionAdd
import jp.juggler.subwaytooter.action.reactionFromAnotherAccount import jp.juggler.subwaytooter.action.reactionFromAnotherAccount
@ -18,6 +19,7 @@ import jp.juggler.subwaytooter.api.entity.TootReaction
import jp.juggler.subwaytooter.api.entity.TootStatus import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.pref.PrefI import jp.juggler.subwaytooter.pref.PrefI
import jp.juggler.subwaytooter.span.NetworkEmojiSpan
import jp.juggler.subwaytooter.util.emojiSizeMode import jp.juggler.subwaytooter.util.emojiSizeMode
import jp.juggler.subwaytooter.util.DecodeOptions import jp.juggler.subwaytooter.util.DecodeOptions
import jp.juggler.subwaytooter.util.NetworkEmojiInvalidator import jp.juggler.subwaytooter.util.NetworkEmojiInvalidator
@ -29,6 +31,7 @@ import jp.juggler.util.ui.attrColor
import jp.juggler.util.ui.getAdaptiveRippleDrawableRound import jp.juggler.util.ui.getAdaptiveRippleDrawableRound
import org.jetbrains.anko.allCaps import org.jetbrains.anko.allCaps
import org.jetbrains.anko.dip import org.jetbrains.anko.dip
import org.jetbrains.anko.wrapContent
private val log = LogCategory("ItemViewHolderReaction") private val log = LogCategory("ItemViewHolderReaction")
@ -42,11 +45,10 @@ fun ItemViewHolder.makeReactionsView(status: TootStatus) {
fun Float.round() = (this + 0.5f).toInt() fun Float.round() = (this + 0.5f).toInt()
val imageScale = 1.5f val imageScale = DecodeOptions.emojiScaleReaction
val buttonHeight = ActMain.boostButtonSize // px val textHeight = (boostButtonSize.toFloat()/2)
val marginBetween = (buttonHeight * 0.05f).round() val marginBetween = (boostButtonSize * 0.05f).round()
val paddingH = (buttonHeight * 0.1f).round() val paddingH = (boostButtonSize * 0.1f).round()
val textHeight = (buttonHeight * 0.7f) / imageScale
val act = this@makeReactionsView.activity // not Button(View).getActivity() val act = this@makeReactionsView.activity // not Button(View).getActivity()
@ -63,7 +65,7 @@ fun ItemViewHolder.makeReactionsView(status: TootStatus) {
if (reactionSet?.isEmpty() != false) { if (reactionSet?.isEmpty() != false) {
val v = View(act).apply { val v = View(act).apply {
layoutParams = FlexboxLayout.LayoutParams(0, buttonHeight) layoutParams = FlexboxLayout.LayoutParams(0, wrapContent)
setPadding(paddingH, 0, paddingH, 0) setPadding(paddingH, 0, paddingH, 0)
} }
box.addView(v) box.addView(v)
@ -85,13 +87,13 @@ fun ItemViewHolder.makeReactionsView(status: TootStatus) {
val b = AppCompatButton(act).apply { val b = AppCompatButton(act).apply {
layoutParams = FlexboxLayout.LayoutParams( layoutParams = FlexboxLayout.LayoutParams(
FlexboxLayout.LayoutParams.WRAP_CONTENT, FlexboxLayout.LayoutParams.WRAP_CONTENT,
buttonHeight, wrapContent,
).apply { ).apply {
if (index > 0) startMargin = marginBetween if (index > 0) startMargin = marginBetween
bottomMargin = dip(3) bottomMargin = dip(3)
} }
gravity = Gravity.CENTER gravity = Gravity.CENTER
minWidthCompat = buttonHeight minWidthCompat = textHeight.round()
background = if (reactionSet.isMyReaction(reaction)) { background = if (reactionSet.isMyReaction(reaction)) {
// 自分がリアクションしたやつは背景を変える // 自分がリアクションしたやつは背景を変える

View File

@ -354,8 +354,8 @@ fun ItemViewHolder.showBoost(
activity, activity,
accessInfo, accessInfo,
decodeEmoji = true, decodeEmoji = true,
enlargeEmoji = 1.5f, enlargeEmoji = DecodeOptions.emojiScaleReaction,
enlargeCustomEmoji = 1.5f, enlargeCustomEmoji = DecodeOptions.emojiScaleReaction,
emojiSizeMode = accessInfo.emojiSizeMode(), emojiSizeMode = accessInfo.emojiSizeMode(),
) )
val ssb = reaction.toSpannableStringBuilder(options, boostStatus) val ssb = reaction.toSpannableStringBuilder(options, boostStatus)

View File

@ -61,7 +61,7 @@ object MisskeyMarkdownDecoder {
fun decodeMarkdown(options: DecodeOptions, src: String?) = fun decodeMarkdown(options: DecodeOptions, src: String?) =
SpannableStringBuilderEx().apply { SpannableStringBuilderEx().apply {
val save = options.enlargeCustomEmoji val save = options.enlargeCustomEmoji
options.enlargeCustomEmoji = 2.5f options.enlargeCustomEmoji = DecodeOptions.emojiScaleMisskey
try { try {
val env = SpanOutputEnv(options, this) val env = SpanOutputEnv(options, this)

View File

@ -124,4 +124,9 @@ object PrefI {
val ipMediaBackground = IntPref("MediaBackground", 1) val ipMediaBackground = IntPref("MediaBackground", 1)
val ipLogSaveLevel = IntPref("LogSaveLevel", Log.WARN) val ipLogSaveLevel = IntPref("LogSaveLevel", Log.WARN)
const val EMOJI_WIDE_AUTO = 0
const val EMOJI_WIDE_ENABLE = 1
const val EMOJI_WIDE_DISABLE = 2
val ipEmojiWideMode = IntPref("EmojiWideMode", EMOJI_WIDE_AUTO)
} }

View File

@ -53,4 +53,10 @@ object PrefS {
val spTimelineSpacing = StringPref("TimelineSpacing", "") val spTimelineSpacing = StringPref("TimelineSpacing", "")
val spEventTextAlpha = StringPref("EventTextAlpha", "") val spEventTextAlpha = StringPref("EventTextAlpha", "")
}
val spEmojiSizeMastodon = StringPref("EmojiSizeMastodon", "100")
val spEmojiSizeMisskey = StringPref("EmojiSizeMisskey", "250")
val spEmojiSizeReaction = StringPref("EmojiSizeReaction", "150")
val spEmojiSizeUserName = StringPref("EmojiSizeUserName", "100")
val spEmojiPixels = StringPref("spEmojiPixels", "128")
}

View File

@ -19,4 +19,5 @@ class StringPref(
defVal != pref.getString(key, defVal) defVal != pref.getString(key, defVal)
fun toInt() = value.toIntOrNull() ?: defVal.toInt() fun toInt() = value.toIntOrNull() ?: defVal.toInt()
fun toFloat() = value.toFloatOrNull() ?: defVal.toFloat()
} }

View File

@ -21,13 +21,13 @@ class NetworkEmojiSpan constructor(
private val url: String, private val url: String,
sizeMode: EmojiSizeMode, sizeMode: EmojiSizeMode,
scale: Float = 1f, scale: Float = 1f,
private val initialAspect:Float? = null, private val initialAspect: Float? = null,
private val errorDrawableId: Int = R.drawable.outline_broken_image_24, private val errorDrawableId: Int = R.drawable.outline_broken_image_24,
) : ReplacementSpan(), AnimatableSpan { ) : ReplacementSpan(), AnimatableSpan {
companion object { companion object {
internal val log = LogCategory("NetworkEmojiSpan") internal val log = LogCategory("NetworkEmojiSpan")
private const val scaleRatio = 1.14f const val scaleRatio = 1.14f
private const val descentRatio = 0.211f private const val descentRatio = 0.211f
// 最大幅 // 最大幅
@ -47,13 +47,23 @@ class NetworkEmojiSpan constructor(
private val rectSrc = Rect() private val rectSrc = Rect()
private var lastMeasuredWidth = 0f
private val emojiImageRect = EmojiImageRect( private val emojiImageRect = EmojiImageRect(
sizeMode = sizeMode, sizeMode = sizeMode,
scale = scale, scale = scale,
scaleRatio = scaleRatio, scaleRatio = scaleRatio,
descentRatio = descentRatio, descentRatio = descentRatio,
maxEmojiWidth = maxEmojiWidth, maxEmojiWidth = maxEmojiWidth,
layout = { _,_-> invalidateCallback?.requestLayout() }, // layout = { _, _ ->
// when (val cb = invalidateCallback) {
// null -> log.w("layoutCb is null")
// else -> {
// log.i("layoutCb requestLayout. url=$url")
// cb.requestLayout()
// }
// }
// }
) )
override fun setInvalidateCallback( override fun setInvalidateCallback(
@ -72,8 +82,12 @@ class NetworkEmojiSpan constructor(
fm: Paint.FontMetricsInt?, fm: Paint.FontMetricsInt?,
): Int { ): Int {
emojiImageRect.updateRect( emojiImageRect.updateRect(
url = url, aspectArg = initialAspect, paint.textSize, baseline = 0f url = url,
aspectArg = initialAspect,
paint.textSize,
baseline = 0f
) )
val height = (emojiImageRect.emojiHeight + 0.5f).toInt() val height = (emojiImageRect.emojiHeight + 0.5f).toInt()
if (fm != null) { if (fm != null) {
val cDescent = (0.5f + height * descentRatio).toInt() val cDescent = (0.5f + height * descentRatio).toInt()
@ -83,7 +97,10 @@ class NetworkEmojiSpan constructor(
if (fm.descent < cDescent) fm.descent = cDescent if (fm.descent < cDescent) fm.descent = cDescent
if (fm.bottom < cDescent) fm.bottom = cDescent if (fm.bottom < cDescent) fm.bottom = cDescent
} }
return (emojiImageRect.emojiWidth + 0.5f).toInt()
val width = emojiImageRect.emojiWidth
lastMeasuredWidth = width
return (width + 0.5f).toInt()
} }
override fun draw( override fun draw(
@ -115,6 +132,7 @@ class NetworkEmojiSpan constructor(
// APNGデータの取得 // APNGデータの取得
val frames = App1.custom_emoji_cache.getFrames(refDrawTarget, url) { val frames = App1.custom_emoji_cache.getFrames(refDrawTarget, url) {
handleFrameLoaded(it)
invalidateCallback.delayInvalidate(0L) invalidateCallback.delayInvalidate(0L)
} ?: return false } ?: return false
@ -144,6 +162,17 @@ class NetworkEmojiSpan constructor(
textPaint.textSize, textPaint.textSize,
baseline.toFloat() baseline.toFloat()
) )
val clipBounds = canvas.clipBounds
val clipWidth = clipBounds.width()
// 最後にgetSizeで返した幅と異なるか、現在のTextViewのClip幅より大きいなら
// 再レイアウトを要求する
if (emojiImageRect.emojiWidth != lastMeasuredWidth ){
log.i("requestLayout by width changed")
invalidateCallback.requestLayout()
}else if(emojiImageRect.emojiWidth > clipWidth) {
log.i("requestLayout by clipWidth ${emojiImageRect.emojiWidth}/${clipWidth}")
invalidateCallback.requestLayout()
}
canvas.save() canvas.save()
try { try {
@ -172,6 +201,12 @@ class NetworkEmojiSpan constructor(
return true return true
} }
private fun handleFrameLoaded(frames: ApngFrames?) {
frames?.aspect?.let {
invalidateCallback?.requestLayout()
}
}
private fun drawError( private fun drawError(
canvas: Canvas, canvas: Canvas,
x: Float, x: Float,
@ -190,7 +225,7 @@ class NetworkEmojiSpan constructor(
url = "", url = "",
aspectArg = srcWidth / srcHeight, aspectArg = srcWidth / srcHeight,
textSize = textPaint.textSize, textSize = textPaint.textSize,
baseline = baseline.toFloat() baseline = baseline.toFloat(),
) )
canvas.save() canvas.save()

View File

@ -70,7 +70,9 @@ import java.util.concurrent.atomic.AtomicReference
// 2022/3/15 63=>64 SavedAccountテーブルに項目追加 // 2022/3/15 63=>64 SavedAccountテーブルに項目追加
// 2023/2/2 64 => 65 PushMessage, AccountNotificationStatus,NotificationShown テーブルの追加。 // 2023/2/2 64 => 65 PushMessage, AccountNotificationStatus,NotificationShown テーブルの追加。
// 2023/2/11 65=>66 LogDataがなければ作る // 2023/2/11 65=>66 LogDataがなければ作る
const val DB_VERSION = 66 // 2023/2/26 66=>67 ImageAspect テーブルの追加
const val DB_VERSION = 67
const val DB_NAME = "app_db" const val DB_NAME = "app_db"
// テーブルのリスト // テーブルのリスト
@ -96,6 +98,7 @@ val TABLE_LIST = arrayOf(
PushMessage.Companion, // v65 PushMessage.Companion, // v65
AccountNotificationStatus.Companion, // v65, AccountNotificationStatus.Companion, // v65,
NotificationShown.Companion, // v65 NotificationShown.Companion, // v65
ImageAspect.Companion, // v67
) )
private val log = LogCategory("AppDatabaseHolder") private val log = LogCategory("AppDatabaseHolder")
@ -201,3 +204,6 @@ val daoTagHistory get() = TagHistory.Access(appDatabase)
val daoUserRelation get() = UserRelation.Access(appDatabase) val daoUserRelation get() = UserRelation.Access(appDatabase)
val daoPushMessage get() = PushMessage.Access(appDatabase) val daoPushMessage get() = PushMessage.Access(appDatabase)
val daoLogData get() = LogData.Access(appDatabase) val daoLogData get() = LogData.Access(appDatabase)
val daoImageAspect by lazy{
ImageAspect.Access(appDatabase)
}

View File

@ -0,0 +1,64 @@
package jp.juggler.subwaytooter.table
import android.content.ContentValues
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import jp.juggler.util.data.*
import jp.juggler.util.log.LogCategory
import kotlin.math.abs
// リスト要素のデータ
class ImageAspect(
var id: Long = 0L,
var url: String = "",
var aspect: Float = 1f,
) {
companion object : TableCompanion {
private val log = LogCategory("ImageAspect")
override val table = "image_aspect"
private const val COL_ID = "_id"
private const val COL_URL = "u"
private const val COL_ASPECT = "aspect"
val columnList = MetaColumns(table, 67).apply {
column(0, COL_ID, "INTEGER PRIMARY KEY")
column(0, COL_URL, "text not null")
column(0, COL_ASPECT, "real not null")
createExtra={
arrayOf(
"create unique index if not exists ${table}_u on $table($COL_URL)",
)
}
}
override fun onDBCreate(db: SQLiteDatabase) {
log.d("onDBCreate!")
columnList.onDBCreate(db)
}
override fun onDBUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
columnList.onDBUpgrade(db,oldVersion,newVersion)
}
}
class Access(val db: SQLiteDatabase) {
fun load(url:String) :Float?=
db.rawQuery(
"select $COL_ASPECT from $table where $COL_URL=?",
arrayOf(url),
).use { cursor->
when {
cursor.moveToNext() -> cursor.getFloat(0)
else -> null
}
}
fun save(url:String,aspect:Float) {
val oldAspect = load(url)
if( oldAspect != null && abs(oldAspect-aspect) <= 1.4E-40F) return
ContentValues().apply {
put(COL_URL, url)
put(COL_ASPECT, aspect)
}.replaceTo(db, table)
}
}
}

View File

@ -10,10 +10,14 @@ import android.os.SystemClock
import com.caverock.androidsvg.SVG import com.caverock.androidsvg.SVG
import jp.juggler.apng.ApngFrames import jp.juggler.apng.ApngFrames
import jp.juggler.subwaytooter.App1 import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.pref.PrefS
import jp.juggler.subwaytooter.table.EmojiCacheDbOpenHelper import jp.juggler.subwaytooter.table.EmojiCacheDbOpenHelper
import jp.juggler.subwaytooter.table.daoImageAspect
import jp.juggler.util.coroutine.EmptyScope
import jp.juggler.util.data.* import jp.juggler.util.data.*
import jp.juggler.util.log.* import jp.juggler.util.log.*
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.* import java.util.*
@ -45,7 +49,7 @@ class CustomEmojiCache(
private class Request( private class Request(
val refTarget: WeakReference<Any>, val refTarget: WeakReference<Any>,
val url: String, val url: String,
val onLoadComplete: () -> Unit, val onLoadComplete: (ApngFrames?) -> Unit,
) )
// APNGデコード済のキャッシュデータ // APNGデコード済のキャッシュデータ
@ -115,7 +119,7 @@ class CustomEmojiCache(
fun getFrames( fun getFrames(
refDrawTarget: WeakReference<Any>?, refDrawTarget: WeakReference<Any>?,
url: String, url: String,
onLoadComplete: () -> Unit, onLoadComplete: (ApngFrames?) -> Unit,
): ApngFrames? { ): ApngFrames? {
try { try {
if (refDrawTarget?.get() == null) { if (refDrawTarget?.get() == null) {
@ -200,7 +204,7 @@ class CustomEmojiCache(
val item = getCached(now, request.url) val item = getCached(now, request.url)
if (item != null) { if (item != null) {
if (item.frames != null) { if (item.frames != null) {
fireCallback(request) fireCallback(request, item.frames)
} }
return@synchronized true return@synchronized true
} }
@ -231,7 +235,7 @@ class CustomEmojiCache(
data = try { data = try {
App1.getHttpCached(request.url) App1.getHttpCached(request.url)
} catch (ex: Throwable) { } catch (ex: Throwable) {
log.w( "get failed. url=${request.url}") log.w("get failed. url=${request.url}")
null null
} }
te = elapsedTime te = elapsedTime
@ -267,7 +271,7 @@ class CustomEmojiCache(
item.frames?.dispose() item.frames?.dispose()
item.frames = frames item.frames = frames
} }
fireCallback(request) fireCallback(request, item.frames)
} }
} }
te = elapsedTime te = elapsedTime
@ -285,8 +289,15 @@ class CustomEmojiCache(
} }
} }
private fun fireCallback(request: Request) { private fun fireCallback(request: Request, frames: ApngFrames?) {
handler.post { request.onLoadComplete() } handler.post { request.onLoadComplete(frames) }
EmptyScope.launch {
try {
frames?.aspect?.let { daoImageAspect.save(request.url, it) }
} catch (ex: Throwable) {
log.e(ex, "aspect save failed.")
}
}
} }
private fun sweepCache(now: Long) { private fun sweepCache(now: Long) {
@ -295,7 +306,7 @@ class CustomEmojiCache(
val over = cache.size - CACHE_MAX val over = cache.size - CACHE_MAX
// 超過した数がある程度大きくなるまで掃除しない // 超過した数がある程度大きくなるまで掃除しない
if (over <= 64) return if (over <= CACHE_MAX / 2) return
// 掃除する候補 // 掃除する候補
val list = ArrayList<CacheItem>() val list = ArrayList<CacheItem>()
@ -319,7 +330,7 @@ class CustomEmojiCache(
private fun decodeAPNG(data: ByteArray, url: String): ApngFrames? { private fun decodeAPNG(data: ByteArray, url: String): ApngFrames? {
val errors = ArrayList<Throwable>() val errors = ArrayList<Throwable>()
val maxSize = 256 val maxSize = PrefS.spEmojiPixels.toInt().clip(16, 1024)
try { try {
// APNGをデコード AWebPも // APNGをデコード AWebPも

View File

@ -9,9 +9,10 @@ import jp.juggler.subwaytooter.api.entity.NicoProfileEmoji
import jp.juggler.subwaytooter.api.entity.TootAttachmentLike import jp.juggler.subwaytooter.api.entity.TootAttachmentLike
import jp.juggler.subwaytooter.api.entity.TootMention import jp.juggler.subwaytooter.api.entity.TootMention
import jp.juggler.subwaytooter.emoji.CustomEmoji import jp.juggler.subwaytooter.emoji.CustomEmoji
import jp.juggler.subwaytooter.util.EmojiSizeMode import jp.juggler.subwaytooter.pref.PrefS
import jp.juggler.subwaytooter.table.HighlightWord import jp.juggler.subwaytooter.table.HighlightWord
import jp.juggler.util.data.WordTrieTree import jp.juggler.util.data.WordTrieTree
import jp.juggler.util.data.clip
import org.jetbrains.anko.collections.forEachReversedByIndex import org.jetbrains.anko.collections.forEachReversedByIndex
class DecodeOptions( class DecodeOptions(
@ -25,8 +26,8 @@ class DecodeOptions(
var emojiMapProfile: Map<String, NicoProfileEmoji>? = null, var emojiMapProfile: Map<String, NicoProfileEmoji>? = null,
var highlightTrie: WordTrieTree? = null, var highlightTrie: WordTrieTree? = null,
var unwrapEmojiImageTag: Boolean = false, var unwrapEmojiImageTag: Boolean = false,
var enlargeCustomEmoji: Float = 1f, var enlargeCustomEmoji: Float = emojiScaleMastodon,
var enlargeEmoji: Float = 1f, var enlargeEmoji: Float = emojiScaleMastodon,
// force use HTML instead of Misskey Markdown // force use HTML instead of Misskey Markdown
var forceHtml: Boolean = false, var forceHtml: Boolean = false,
var mentionFullAcct: Boolean = false, var mentionFullAcct: Boolean = false,
@ -36,6 +37,18 @@ class DecodeOptions(
var authorDomain: HostAndDomain? = null, var authorDomain: HostAndDomain? = null,
var emojiSizeMode: EmojiSizeMode = EmojiSizeMode.Square, var emojiSizeMode: EmojiSizeMode = EmojiSizeMode.Square,
) { ) {
companion object {
var emojiScaleMastodon = 1f
var emojiScaleMisskey = 1f
var emojiScaleReaction = 1f
var emojiScaleUserName = 1f
fun reloadEmojiScale() {
emojiScaleMastodon = PrefS.spEmojiSizeMastodon.toFloat().div(100f).clip(0.5f, 5f)
emojiScaleMisskey = PrefS.spEmojiSizeMisskey.toFloat().div(100f).clip(0.5f, 5f)
emojiScaleReaction = PrefS.spEmojiSizeReaction.toFloat().div(100f).clip(0.5f, 5f)
emojiScaleUserName = PrefS.spEmojiSizeUserName.toFloat().div(100f).clip(0.5f, 5f)
}
}
internal fun isMediaAttachment(url: String?): Boolean = internal fun isMediaAttachment(url: String?): Boolean =
url?.let { u -> attachmentList?.any { it.hasUrl(u) } } ?: false url?.let { u -> attachmentList?.any { it.hasUrl(u) } } ?: false

View File

@ -380,11 +380,11 @@ object EmojiDecoder {
val userHost = cols.elementAtOrNull(1) val userHost = cols.elementAtOrNull(1)
?: options.authorDomain?.apiHost?.ascii ?: options.authorDomain?.apiHost?.ascii
?: apiHostAscii ?: apiHostAscii
log.i( // log.i(
"decodeEmoji Misskey13 c0=${cols.elementAtOrNull(0)} c1=${ // "decodeEmoji Misskey13 c0=${cols.elementAtOrNull(0)} c1=${
cols.elementAtOrNull(1) // cols.elementAtOrNull(1)
} apiHostAscii=$apiHostAscii, userHost=$userHost" // } apiHostAscii=$apiHostAscii, userHost=$userHost"
) // )
when { when {
// 絵文字プロクシを利用できない // 絵文字プロクシを利用できない

View File

@ -2,7 +2,9 @@ package jp.juggler.subwaytooter.util
import android.graphics.RectF import android.graphics.RectF
import jp.juggler.subwaytooter.api.entity.TootInstance import jp.juggler.subwaytooter.api.entity.TootInstance
import jp.juggler.subwaytooter.pref.PrefI
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.daoImageAspect
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
import kotlin.math.min import kotlin.math.min
@ -11,14 +13,19 @@ enum class EmojiSizeMode {
Wide, Wide,
} }
fun SavedAccount?.emojiSizeMode(): EmojiSizeMode { fun SavedAccount?.emojiSizeMode(): EmojiSizeMode =
val ti = this?.let { TootInstance.getCached(it) } when (PrefI.ipEmojiWideMode.value) {
return when { PrefI.EMOJI_WIDE_ENABLE -> EmojiSizeMode.Wide
ti == null -> EmojiSizeMode.Square PrefI.EMOJI_WIDE_DISABLE -> EmojiSizeMode.Square
ti.isMisskey || !ti.fedibirdCapabilities.isNullOrEmpty() -> EmojiSizeMode.Wide else /* PrefI.EMOJI_WIDE_AUTO */ -> {
else -> EmojiSizeMode.Square val ti = this?.let { TootInstance.getCached(it) }
when {
ti == null -> EmojiSizeMode.Square
ti.isMisskey || !ti.fedibirdCapabilities.isNullOrEmpty() -> EmojiSizeMode.Wide
else -> EmojiSizeMode.Square
}
}
} }
}
/** /**
* カスタム絵文字のSpanやビューの描画領域やサイズを計算する * カスタム絵文字のSpanやビューの描画領域やサイズを計算する
@ -29,7 +36,7 @@ class EmojiImageRect(
val scaleRatio: Float = 1f, val scaleRatio: Float = 1f,
val descentRatio: Float = 0f, val descentRatio: Float = 0f,
val maxEmojiWidth: Float, val maxEmojiWidth: Float,
val layout: (Int, Int) -> Unit, // val layout: (Int, Int) -> Unit,
) { ) {
companion object { companion object {
private val log = LogCategory("EmojiImageRect") private val log = LogCategory("EmojiImageRect")
@ -42,8 +49,6 @@ class EmojiImageRect(
var emojiHeight = 0f var emojiHeight = 0f
var transY = 0f var transY = 0f
var lastWidth: Float? = null
/** /**
* lastAspect に基づいて rectDst transY を更新する * lastAspect に基づいて rectDst transY を更新する
*/ */
@ -69,9 +74,13 @@ class EmojiImageRect(
) { ) {
this.emojiHeight = h this.emojiHeight = h
val aspect = when (aspectArg) { val aspect = when (aspectArg) {
null -> imageAspectCache[url] ?: 1f null -> {
imageAspectCache[url]
?: daoImageAspect.load(url)?.also { imageAspectCache[url] = it }
?: 1f
}
else -> { else -> {
imageAspectCache.put(url, aspectArg) if (url.isNotEmpty()) imageAspectCache[url] = aspectArg
aspectArg aspectArg
} }
}.takeIf { it > 0f } ?: 1f }.takeIf { it > 0f } ?: 1f
@ -89,7 +98,6 @@ class EmojiImageRect(
} }
else -> { else -> {
emojiWidth = emojiHeight
// 絵文字のアスペクト比から描画範囲の幅と高さを決める // 絵文字のアスペクト比から描画範囲の幅と高さを決める
val dstWidth: Float val dstWidth: Float
val dstHeight: Float val dstHeight: Float
@ -103,14 +111,8 @@ class EmojiImageRect(
val dstX = (emojiHeight - dstWidth) / 2f val dstX = (emojiHeight - dstWidth) / 2f
val dstY = (emojiHeight - dstHeight) / 2f val dstY = (emojiHeight - dstHeight) / 2f
rectDst.set(dstX, dstY, dstX + dstWidth, dstY + dstHeight) rectDst.set(dstX, dstY, dstX + dstWidth, dstY + dstHeight)
emojiWidth = emojiHeight
} }
} }
// 出力サイズが変化したならrequestLayout
val newWidth = emojiWidth
if (lastWidth != null && lastWidth != newWidth) {
log.i("updateRect: width changed. $lastWidth$newWidth")
layout((emojiWidth + 0.5f).toInt(), (emojiHeight + 0.5f).toInt())
}
lastWidth = newWidth
} }
} }

View File

@ -8,6 +8,7 @@ import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.span.AnimatableSpan import jp.juggler.subwaytooter.span.AnimatableSpan
import jp.juggler.subwaytooter.span.AnimatableSpanInvalidator import jp.juggler.subwaytooter.span.AnimatableSpanInvalidator
import jp.juggler.util.data.clip import jp.juggler.util.data.clip
import jp.juggler.util.log.LogCategory
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
class NetworkEmojiInvalidator( class NetworkEmojiInvalidator(
@ -15,6 +16,9 @@ class NetworkEmojiInvalidator(
internal val view: TextView, internal val view: TextView,
// val parent:View? = null, // val parent:View? = null,
) : AnimatableSpanInvalidator { ) : AnimatableSpanInvalidator {
companion object {
private val log = LogCategory("NetworkEmojiInvalidator")
}
var text: CharSequence var text: CharSequence
get() = view.text get() = view.text

View File

@ -55,25 +55,23 @@ class NetworkEmojiView(
scaleRatio = 1f, scaleRatio = 1f,
descentRatio = 0f, descentRatio = 0f,
maxEmojiWidth = maxEmojiWidth, maxEmojiWidth = maxEmojiWidth,
layout = { w, h -> // layout = { w, h ->
val lp = layoutParams // val lp = layoutParams
lp.width = w // lp.width = w
lp.height = h // lp.height = h
layoutParams = lp // layoutParams = lp
requestLayout() // requestLayout()
}, // },
) )
fun setEmoji( fun setEmoji(
url: String?, url: String?,
initialAspect: Float?, initialAspect: Float?,
defaultWidth: Int,
defaultHeight: Int, defaultHeight: Int,
) { ) {
this.url = url this.url = url
mPaint.isFilterBitmap = true mPaint.isFilterBitmap = true
invalidate() invalidate()
emojiImageRect.lastWidth = null
emojiImageRect.updateRect( emojiImageRect.updateRect(
url = url ?: "", url = url ?: "",
aspectArg = initialAspect, aspectArg = initialAspect,
@ -130,6 +128,13 @@ class NetworkEmojiView(
aspectArg = srcAspect, aspectArg = srcAspect,
h = dstHeight, h = dstHeight,
) )
val width = (emojiImageRect.emojiWidth + 0.5f).toInt()
if (width != layoutParams.width) {
val lp = layoutParams
lp.width = width
lp.height = (emojiImageRect.emojiHeight + 0.5f).toInt()
layoutParams = lp
}
canvas.drawBitmap(b, rectSrc, emojiImageRect.rectDst, mPaint) canvas.drawBitmap(b, rectSrc, emojiImageRect.rectDst, mPaint)

View File

@ -1253,7 +1253,7 @@
<string name="approved">承認されました。</string> <string name="approved">承認されました。</string>
<string name="account_list">アカウント一覧</string> <string name="account_list">アカウント一覧</string>
<string name="show_username_on_filtered_post">フィルタされた投稿にユーザ名を表示する</string> <string name="show_username_on_filtered_post">フィルタされた投稿にユーザ名を表示する</string>
<string name="log_save_level" >ログ収集レベル</string> <string name="log_save_level">ログ収集レベル</string>
<string name="send_log_email">ログを電子メールで送信する</string> <string name="send_log_email">ログを電子メールで送信する</string>
<string name="bug_report">バグ報告</string> <string name="bug_report">バグ報告</string>
<string name="bug_report_desc">アプリは過去数日間のログを保持していますが、勝手に外部に送信することはありません。ユーザが明示的にログを送信する操作した時だけ参照されます。</string> <string name="bug_report_desc">アプリは過去数日間のログを保持していますが、勝手に外部に送信することはありません。ユーザが明示的にログを送信する操作した時だけ参照されます。</string>
@ -1261,4 +1261,12 @@
<string name="image_alpha_too_low">背景画像のアルファが低すぎます。 画像を見ることができますか?</string> <string name="image_alpha_too_low">背景画像のアルファが低すぎます。 画像を見ることができますか?</string>
<string name="enable_deprecated_something">陳腐化した何かを有効にする</string> <string name="enable_deprecated_something">陳腐化した何かを有効にする</string>
<string name="misskey_support_end">Misskeyサポートは終了しました</string> <string name="misskey_support_end">Misskeyサポートは終了しました</string>
<string name="emoji_size_mastodon">(Mastodon)カスタム絵文字の大きさ(単位:%、デフォルト:100)</string>
<string name="emoji_size_misskey">(Misskey)カスタム絵文字の大きさ(単位:%、デフォルト:250)</string>
<string name="emoji_size_reaction">リアクションの絵文字の大きさ(単位:%、デフォルト:150)</string>
<string name="emoji_size_user_name">ユーザ名の絵文字の大きさ(単位:%、デフォルト:100)</string>
<string name="emoji_wide_mode">ワイドカスタム絵文字</string>
<string name="enabled">有効</string>
<string name="disabled">無効</string>
<string name="emoji_texture_pixels">絵文字テクスチャの最大ピクセル数(単位:ピクセル、デフォルト: 256。 タスクキルが必要。端末のRAMが少ない場合は64程度まで下げることをお勧めします)</string>
</resources> </resources>

View File

@ -1269,4 +1269,12 @@
<string name="image_alpha_too_low">background image alpha is too low. can you view the image?</string> <string name="image_alpha_too_low">background image alpha is too low. can you view the image?</string>
<string name="enable_deprecated_something">Enable deprecated something</string> <string name="enable_deprecated_something">Enable deprecated something</string>
<string name="misskey_support_end">Misskey support has ended</string> <string name="misskey_support_end">Misskey support has ended</string>
<string name="emoji_size_mastodon">(Mastodon)Custom emoji scaling(Unit:%. default:100)</string>
<string name="emoji_size_misskey">(Misskey)Custom emoji scaling(Unit:%。default:250)</string>
<string name="emoji_size_reaction">Reaction emoji scaling(Unit:%。default:150)</string>
<string name="emoji_size_user_name">User name emoji scaling(Unit:%。default:100)</string>
<string name="emoji_wide_mode">Wide custom emoji</string>
<string name="enabled">Enabled</string>
<string name="disabled">Disabled</string>
<string name="emoji_texture_pixels">Emoji texture max pixels(Unix:pixels, default: 256. task kill required. reduce to 64 if your device\'s RAM is not enough)</string>
</resources> </resources>

View File

@ -119,7 +119,7 @@ for my $lang ( sort keys %langs ){
my $sv = $value; my $sv = $value;
$sv =~ s/(%\d+\$[\d\.]*[sdxf])//g; $sv =~ s/(%\d+\$[\d\.]*[sdxf])//g;
# Unit:%. や %% を除外したい # Unit:%. や %% を除外したい
$sv =~ s/%[\s.。%]//g; $sv =~ s/%[\s.,%]//g;
if( $sv =~ /%/ ){ if( $sv =~ /%/ ){
$hasError =1; $hasError =1;
print "!! ($lang)$name : broken param: $sv // $value\n"; print "!! ($lang)$name : broken param: $sv // $value\n";