(Misskey 13)絵文字ピッカーと投稿中のローカルなカスタム絵文字の表示

This commit is contained in:
tateisu 2023-01-11 01:40:26 +09:00
parent aa17a0a86e
commit 07daa2f7ef
28 changed files with 365 additions and 119 deletions

View File

@ -7,6 +7,7 @@
<module name="SubwayTooter.app" target="11" />
<module name="SubwayTooter.colorpicker" target="11" />
<module name="SubwayTooter.emoji" target="11" />
<module name="SubwayTooter.icon_material_symbols" target="11" />
<module name="SubwayTooter.sample_apng" target="11" />
</bytecodeTargetLevel>
</component>

View File

@ -16,6 +16,7 @@
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/colorpicker" />
<option value="$PROJECT_DIR$/emoji" />
<option value="$PROJECT_DIR$/icon_material_symbols" />
<option value="$PROJECT_DIR$/sample_apng" />
</set>
</option>

View File

@ -22,6 +22,10 @@
<module fileurl="file://$PROJECT_DIR$/.idea/modules/emoji/SubwayTooter.emoji.androidTest.iml" filepath="$PROJECT_DIR$/.idea/modules/emoji/SubwayTooter.emoji.androidTest.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/emoji/SubwayTooter.emoji.main.iml" filepath="$PROJECT_DIR$/.idea/modules/emoji/SubwayTooter.emoji.main.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/emoji/SubwayTooter.emoji.unitTest.iml" filepath="$PROJECT_DIR$/.idea/modules/emoji/SubwayTooter.emoji.unitTest.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/icon_material_symbols/SubwayTooter.icon_material_symbols.iml" filepath="$PROJECT_DIR$/.idea/modules/icon_material_symbols/SubwayTooter.icon_material_symbols.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/icon_material_symbols/SubwayTooter.icon_material_symbols.androidTest.iml" filepath="$PROJECT_DIR$/.idea/modules/icon_material_symbols/SubwayTooter.icon_material_symbols.androidTest.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/icon_material_symbols/SubwayTooter.icon_material_symbols.main.iml" filepath="$PROJECT_DIR$/.idea/modules/icon_material_symbols/SubwayTooter.icon_material_symbols.main.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/icon_material_symbols/SubwayTooter.icon_material_symbols.unitTest.iml" filepath="$PROJECT_DIR$/.idea/modules/icon_material_symbols/SubwayTooter.icon_material_symbols.unitTest.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/sample_apng/SubwayTooter.sample_apng.iml" filepath="$PROJECT_DIR$/.idea/modules/sample_apng/SubwayTooter.sample_apng.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/sample_apng/SubwayTooter.sample_apng.androidTest.iml" filepath="$PROJECT_DIR$/.idea/modules/sample_apng/SubwayTooter.sample_apng.androidTest.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/sample_apng/SubwayTooter.sample_apng.main.iml" filepath="$PROJECT_DIR$/.idea/modules/sample_apng/SubwayTooter.sample_apng.main.iml" />

View File

@ -1,6 +1,5 @@
package jp.juggler.subwaytooter
import android.Manifest
import android.annotation.SuppressLint
import android.app.DownloadManager
import android.content.ClipData
@ -296,6 +295,7 @@ class ActMediaViewer : AppCompatActivity(), View.OnClickListener {
App1.initEdgeToEdge(this)
views.pbvImage.background = MediaBackgroundDrawable(
context = views.root.context,
tileStep = tileStep,
kind = MediaBackgroundDrawable.Kind.fromIndex(PrefI.ipMediaBackground(this))
)
@ -811,11 +811,12 @@ class ActMediaViewer : AppCompatActivity(), View.OnClickListener {
private fun mediaBackgroundDialog() {
val ad = ActionsDialog()
for (k in MediaBackgroundDrawable.Kind.values()) {
if (!k.isMediaBackground) continue
ad.addAction(k.name) {
val idx = k.toIndex()
appPref.edit().put(PrefI.ipMediaBackground, idx).apply()
views.pbvImage.background = MediaBackgroundDrawable(
context = views.root.context,
tileStep = tileStep,
kind = k
)

View File

@ -465,22 +465,29 @@ class App1 : Application() {
suspend fun getHttpCachedString(
url: String,
accessInfo: SavedAccount? = null,
misskeyPost: Boolean = false,
builderBlock: (Request.Builder) -> Unit = {},
): String? {
val response: Response
try {
val request_builder = Request.Builder()
.url(url)
.cacheControl(CACHE_CONTROL)
val access_token = accessInfo?.getAccessToken()
if (access_token?.isNotEmpty() == true) {
request_builder.header("Authorization", "Bearer $access_token")
val request_builder = when {
misskeyPost && accessInfo?.isMisskey == true ->
accessInfo.putMisskeyApiToken().toPostRequestBuilder()
.url(url)
.cacheControl(CACHE_CONTROL)
else ->
Request.Builder()
.url(url)
.cacheControl(CACHE_CONTROL)
.also {
val access_token = accessInfo?.getAccessToken()
if (access_token?.isNotEmpty() == true) {
it.header("Authorization", "Bearer $access_token")
}
}
}
builderBlock(request_builder)
val call = ok_http_client2.newCall(request_builder.build())
response = call.await()
} catch (ex: Throwable) {

View File

@ -498,7 +498,6 @@ class TootApiClient(
try {
if (!sendRequest(result) {
val url = "https://${apiHost?.ascii}$path"
requestBuilder.url(url)
@ -508,7 +507,8 @@ class TootApiClient(
requestBuilder.build()
.also { log.d("request: ${it.method} $url") }
}) return result
}
) return result
return parseJson(result)
} finally {

View File

@ -5,7 +5,6 @@ import jp.juggler.util.JsonArray
import jp.juggler.util.JsonException
import jp.juggler.util.JsonObject
import jp.juggler.util.LogCategory
import java.util.HashMap
object EntityUtil {
val log = LogCategory("EntityUtil")
@ -122,8 +121,9 @@ inline fun <reified K, reified V> parseMapOrNull(
}
inline fun <reified K, reified V> parseMapOrNull(
factory: (host: Host, src: JsonObject) -> V,
host: Host,
factory: (apDomain: Host, apiHost: Host, src: JsonObject) -> V,
apDomain: Host,
apiHost: Host,
src: JsonArray?,
log: LogCategory = EntityUtil.log,
): HashMap<K, V>? where V : Mappable<K> {
@ -132,7 +132,7 @@ inline fun <reified K, reified V> parseMapOrNull(
if (size > 0) {
val dst = HashMap<K, V>()
for (i in 0 until size) {
val item = parseItem(factory, host, src.jsonObject(i), log)
val item = parseItem(factory, apDomain, apiHost, src.jsonObject(i), log)
if (item != null) dst[item.mapKey] = item
}
if (dst.isNotEmpty()) return dst
@ -183,6 +183,22 @@ inline fun <P, reified T> parseItem(
}
}
inline fun <P1, P2, reified T> parseItem(
factory: (p1: P1, p2: P2, src: JsonObject) -> T,
p1: P1,
p2: P2,
src: JsonObject?,
log: LogCategory = EntityUtil.log,
): T? {
if (src == null) return null
return try {
factory(p1, p2, src)
} catch (ex: Throwable) {
log.e(ex, "${T::class.simpleName} parse failed.")
null
}
}
inline fun <reified T> parseItem(
factory: (serviceType: ServiceType, src: JsonObject) -> T,
serviceType: ServiceType,
@ -198,9 +214,9 @@ inline fun <reified T> parseItem(
}
}
inline fun <reified T> parseList(
factory: (parser: TootParser, src: JsonObject) -> T,
parser: TootParser,
inline fun <P1, reified T> parseListP1(
factory: (p1: P1, src: JsonObject) -> T,
p1: P1,
src: JsonArray?,
log: LogCategory = EntityUtil.log,
): ArrayList<T> {
@ -210,7 +226,28 @@ inline fun <reified T> parseList(
if (src_length > 0) {
dst.ensureCapacity(src_length)
for (i in src.indices) {
val item = parseItem(factory, parser, src.jsonObject(i), log)
val item = parseItem(factory, p1, src.jsonObject(i), log)
if (item != null) dst.add(item)
}
}
}
return dst
}
inline fun <P1, P2, reified T> parseListP2(
factory: (p1: P1, p2: P2, src: JsonObject) -> T,
p1: P1,
p2: P2,
src: JsonArray?,
log: LogCategory = EntityUtil.log,
): ArrayList<T> {
val dst = ArrayList<T>()
if (src != null) {
val src_length = src.size
if (src_length > 0) {
dst.ensureCapacity(src_length)
for (i in src.indices) {
val item = parseItem(factory, p1, p2, src.jsonObject(i), log)
if (item != null) dst.add(item)
}
}

View File

@ -4,7 +4,7 @@ import jp.juggler.subwaytooter.emoji.CustomEmoji
import jp.juggler.util.JsonObject
import jp.juggler.util.LogCategory
class MisskeyNoteUpdate(apDomain: Host, src: JsonObject) {
class MisskeyNoteUpdate(apDomain: Host, apiHost: Host, src: JsonObject) {
companion object {
private val log = LogCategory("MisskeyNoteUpdate")
}
@ -37,7 +37,7 @@ class MisskeyNoteUpdate(apDomain: Host, src: JsonObject) {
userId = EntityId.mayDefault(body2.string("userId"))
emoji = body2.jsonObject("emoji")?.let {
try {
CustomEmoji.decodeMisskey(apDomain, it)
CustomEmoji.decodeMisskey(apDomain, apiHost, it)
} catch (ex: Throwable) {
log.e(ex, "can't parse custom emoji.")
null

View File

@ -138,6 +138,7 @@ open class TootAccount(parser: TootParser, src: JsonObject) : HostAndDomain {
parseMapOrNull(
CustomEmoji.decodeMisskey,
parser.apDomain,
parser.apiHost,
src.jsonArray("emojis")
)
this.profile_emojis = null
@ -276,8 +277,12 @@ open class TootAccount(parser: TootParser, src: JsonObject) : HostAndDomain {
else -> {
// 絵文字データは先に読んでおく
this.custom_emojis =
parseMapOrNull(CustomEmoji.decode, parser.apDomain, src.jsonArray("emojis"))
this.custom_emojis = parseMapOrNull(
CustomEmoji.decode,
parser.apDomain,
parser.apiHost,
src.jsonArray("emojis")
)
this.profile_emojis = when (val o = src["profile_emojis"]) {
is JsonArray -> parseMapOrNull(::NicoProfileEmoji, o, TootStatus.log)

View File

@ -7,7 +7,6 @@ import jp.juggler.subwaytooter.util.DecodeOptions
import jp.juggler.util.JsonObject
import jp.juggler.util.LogCategory
import jp.juggler.util.notEmpty
import java.util.*
class TootAnnouncement(parser: TootParser, src: JsonObject) {
@ -44,8 +43,13 @@ class TootAnnouncement(parser: TootParser, src: JsonObject) {
init {
// 絵文字マップはすぐ後で使うので、最初の方で読んでおく
this.custom_emojis =
parseMapOrNull(CustomEmoji.decode, parser.apDomain, src.jsonArray("emojis"), log)
this.custom_emojis = parseMapOrNull(
CustomEmoji.decode,
parser.apDomain,
parser.apiHost,
src.jsonArray("emojis"),
log
)
this.tags = TootTag.parseListOrNull(parser, src.jsonArray("tags"))

View File

@ -239,11 +239,7 @@ class TootInstance(parser: TootParser, src: JsonObject) {
val canUseReference: Boolean?
get() = fedibird_capabilities?.contains("status_reference")
fun versionGE(check: VersionString): Boolean {
if (decoded_version.isEmpty || check.isEmpty) return false
val i = VersionString.compare(decoded_version, check)
return i >= 0
}
fun versionGE(check: VersionString) = decoded_version.ge(check)
companion object {
private val log = LogCategory("TootInstance")

View File

@ -238,13 +238,13 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() {
// お気に入りカラムなどではパース直後に変更することがある
// 絵文字マップはすぐ後で使うので、最初の方で読んでおく
this.custom_emojis =
parseMapOrNull(
CustomEmoji.decodeMisskey,
parser.apDomain,
src.jsonArray("emojis"),
log
)
this.custom_emojis = parseMapOrNull(
CustomEmoji.decodeMisskey,
parser.apDomain,
parser.apiHost,
src.jsonArray("emojis"),
log
)
this.profile_emojis = null
val who = parser.account(src.jsonObject("user"))
@ -581,13 +581,13 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() {
this.created_at = src.string("created_at")
// 絵文字マップはすぐ後で使うので、最初の方で読んでおく
this.custom_emojis =
parseMapOrNull(
CustomEmoji.decode,
parser.apDomain,
src.jsonArray("emojis"),
log
)
this.custom_emojis = parseMapOrNull(
CustomEmoji.decode,
parser.apDomain,
parser.apiHost,
src.jsonArray("emojis"),
log
)
this.profile_emojis = when (val o = src["profile_emojis"]) {
is JsonArray -> parseMapOrNull(::NicoProfileEmoji, o, log)
@ -809,9 +809,9 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() {
}
private fun mergeMentions(
mentions1: java.util.ArrayList<TootMention>?,
mentions2: java.util.ArrayList<TootMention>?,
): java.util.ArrayList<TootMention>? {
mentions1: List<TootMention>?,
mentions2: List<TootMention>?,
): ArrayList<TootMention>? {
val size = (mentions1?.size ?: 0) + (mentions2?.size ?: 0)
if (size == 0) return null
val dst = ArrayList<TootMention>(size)

View File

@ -466,7 +466,9 @@ val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_sett
sw(PrefB.bpUseInternalMediaViewer, R.string.use_internal_media_viewer)
spinner(PrefI.ipMediaBackground, R.string.background_pattern) {
MediaBackgroundDrawable.Kind.values().map { it.name }
MediaBackgroundDrawable.Kind.values()
.filter{it.isMediaBackground}
.map { it.name }
}
sw(PrefB.bpPriorLocalURL, R.string.prior_local_url_when_open_attachment)

View File

@ -23,6 +23,7 @@ import com.google.android.flexbox.FlexboxLayout
import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.databinding.EmojiPickerDialogBinding
import jp.juggler.subwaytooter.drawable.MediaBackgroundDrawable
import jp.juggler.subwaytooter.emoji.*
import jp.juggler.subwaytooter.global.appPref
import jp.juggler.subwaytooter.pref.PrefB
@ -574,7 +575,7 @@ private class EmojiPicker(
category.createFiltered(keywordLower)
.takeIf { it.items.isNotEmpty() }
}.forEach {
if (selectedCategory == null) add(it)
if( it.category == EmojiCategory.Custom || selectedCategory == null) add(it)
addAll(it.items)
val mod = it.items.size % gridCols
if (mod > 0) {
@ -743,8 +744,12 @@ private class EmojiPicker(
views.etFilter.addTextChangedListener(textWatcher)
showFiltered(null, null)
val density = activity.resources.displayMetrics.density
views.giGrid.intercept = { handleTouch(it, wasIntercept = false) }
views.giGrid.touch = { handleTouch(it, wasIntercept = true) }

View File

@ -1,22 +1,35 @@
package jp.juggler.subwaytooter.drawable
import android.content.Context
import android.graphics.*
import android.graphics.drawable.Drawable
import androidx.annotation.ColorInt
import jp.juggler.subwaytooter.R
import jp.juggler.util.attrColor
import kotlin.math.min
class MediaBackgroundDrawable(
private val context: Context,
private val tileStep: Int,
private val kind: Kind
private val kind: Kind,
) : Drawable() {
enum class Kind(@ColorInt val c1: Int, @ColorInt val c2: Int = 0) {
Black(Color.BLACK),
BlackTile(Color.BLACK, Color.BLACK or 0x202020),
Grey(Color.BLACK or 0x787878),
GreyTile(Color.BLACK or 0x707070, Color.BLACK or 0x808080),
White(Color.WHITE),
WhiteTile(Color.WHITE, Color.BLACK or 0xe0e0e0),
enum class Kind(
val c1: Context.() -> Int,
val c2: Context.() -> Int,
val isMediaBackground: Boolean = true,
) {
Black({ Color.BLACK }, { 0 }),
BlackTile({ Color.BLACK }, { Color.BLACK or 0x202020 }),
Grey({ Color.BLACK or 0x787878 }, { 0 }),
GreyTile({ Color.BLACK or 0x707070 }, { Color.BLACK or 0x808080 }),
White({ Color.WHITE }, { 0 }),
WhiteTile({ Color.WHITE }, { Color.BLACK or 0xe0e0e0 }),
EmojiPickerBg(
{ attrColor(R.attr.colorWindowBackground) },
{ attrColor(R.attr.colorTimeSmall) },
isMediaBackground = false
),
;
@ -51,8 +64,8 @@ class MediaBackgroundDrawable(
override fun draw(canvas: Canvas) {
val bounds = this.bounds
val c1 = kind.c1
val c2 = kind.c2
val c1 = kind.c1.invoke(context)
val c2 = kind.c2.invoke(context)
if (c2 == 0) {
paint.color = c1
canvas.drawRect(bounds, paint)

View File

@ -7,7 +7,6 @@ import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.util.JsonArray
import jp.juggler.util.JsonObject
import jp.juggler.util.notEmpty
import java.util.*
sealed interface EmojiBase
@ -81,7 +80,7 @@ class CustomEmoji(
companion object {
val decode: (Host, JsonObject) -> CustomEmoji = { apDomain, src ->
val decode: (Host, Host, JsonObject) -> CustomEmoji = { apDomain, _, src ->
CustomEmoji(
apDomain = apDomain,
shortcode = src.stringOrThrow("shortcode"),
@ -92,7 +91,7 @@ class CustomEmoji(
)
}
val decodeMisskey: (Host, JsonObject) -> CustomEmoji = { apDomain, src ->
val decodeMisskey: (Host, Host, JsonObject) -> CustomEmoji = { apDomain, _, src ->
val url = src.string("url") ?: error("missing url")
CustomEmoji(
@ -105,6 +104,19 @@ class CustomEmoji(
)
}
val decodeMisskey13: (Host, Host, JsonObject) -> CustomEmoji = { apDomain, apiHost, src ->
val name = src.string("name") ?: error("missing name")
val url = "https://${apiHost.ascii}/emoji/$name.webp"
CustomEmoji(
apDomain = apDomain,
shortcode = name,
url = url,
staticUrl = url,
aliases = parseAliases(src.jsonArray("aliases")),
category = src.string("category"),
)
}
private fun parseAliases(src: JsonArray?): ArrayList<String>? {
var dst = null as ArrayList<String>?
if (src != null) {

View File

@ -197,7 +197,11 @@ class StreamConnection(
log.e("$name handleMisskeyMessage: noteUpdated body is null")
return
}
fireNoteUpdated(MisskeyNoteUpdate(acctGroup.account.apDomain, body), channelId)
fireNoteUpdated(MisskeyNoteUpdate(
acctGroup.account.apDomain,
acctGroup.account.apiHost,
body
), channelId)
}
"notification" -> {

View File

@ -4,7 +4,7 @@ import android.content.Context
import android.os.Handler
import android.os.SystemClock
import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.api.entity.parseList
import jp.juggler.subwaytooter.api.entity.parseListP2
import jp.juggler.subwaytooter.emoji.CustomEmoji
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.*
@ -35,6 +35,7 @@ class CustomEmojiLister(
val key: String,
var list: List<CustomEmoji>,
var listWithAliases: List<CustomEmoji>,
var mapShortCode: Map<String, CustomEmoji>,
// ロードした時刻
var timeUpdate: Long = elapsedTime,
// 参照された時刻
@ -63,7 +64,12 @@ class CustomEmojiLister(
// エラーキャッシュ
internal val cacheError = ConcurrentHashMap<String, Long>()
private val cacheErrorItem = CacheItem("error", emptyList(), emptyList())
private val cacheErrorItem = CacheItem(
key = "error",
list = emptyList(),
listWithAliases = emptyList(),
mapShortCode = emptyMap(),
)
// ロード要求
internal val queue = ConcurrentLinkedQueue<Request>()
@ -75,18 +81,21 @@ class CustomEmojiLister(
cacheError.clear()
}
private fun getCached(now: Long, accessInfo: SavedAccount): CacheItem? {
val host = accessInfo.apiHost.ascii
private fun getCached(now: Long, accessInfo: SavedAccount) =
getCached(now, accessInfo.apiHost.ascii)
private fun getCached(now: Long, apiHostAscii: String?): CacheItem? {
apiHostAscii ?: return null
// 成功キャッシュ
val item = cache[host]
val item = cache[apiHostAscii]
if (item != null && now - item.timeUpdate <= ERROR_EXPIRE) {
item.timeUsed = now
return item
}
// エラーキャッシュ
val timeError = cacheError[host]
val timeError = cacheError[apiHostAscii]
if (timeError != null && now < timeError + ERROR_EXPIRE) {
return cacheErrorItem
}
@ -143,6 +152,9 @@ class CustomEmojiLister(
}
}
fun getCachedEmoji(apiHostAscii: String?, shortcode: String): CustomEmoji? =
getCached(elapsedTime, apiHostAscii)?.mapShortCode?.get(shortcode)
private inner class Worker : WorkerBase() {
override fun cancel() {
@ -186,39 +198,92 @@ class CustomEmojiLister(
val accessInfo = request.accessInfo
val cacheKey = accessInfo.apiHost.ascii
val data = if (accessInfo.isMisskey) {
// v12のmetaからemojisをパース
suspend fun misskeyEmojis12(): List<CustomEmoji>? =
App1.getHttpCachedString(
"https://$cacheKey/api/meta",
accessInfo = accessInfo
) { builder ->
builder.post(JsonObject().toRequestBody())
}?.decodeJsonObject()
?.jsonArray("emojis")
?.let { emojis12 ->
parseListP2(
CustomEmoji.decodeMisskey,
accessInfo.apDomain,
accessInfo.apiHost,
emojis12,
)
}
// v13のemojisを読む
suspend fun misskeyEmojis13(): List<CustomEmoji>? =
App1.getHttpCachedString(
"https://$cacheKey/api/emojis",
accessInfo = accessInfo,
misskeyPost = true,
) { builder ->
builder.post(JsonObject().toRequestBody())
}
} else {
?.decodeJsonObject()
?.jsonArray("emojis")
?.let { emojis13 ->
parseListP2(
CustomEmoji.decodeMisskey13,
accessInfo.apDomain,
accessInfo.apiHost,
emojis13,
)
}
// マストドンのカスタム絵文字一覧を読む
suspend fun mastodonEmojis() =
App1.getHttpCachedString(
"https://$cacheKey/api/v1/custom_emojis",
accessInfo = accessInfo
)
}
var list: List<CustomEmoji>? = null
var listWithAlias: List<CustomEmoji>? = null
if (data != null) {
val a = decodeEmojiList(data, accessInfo)
list = a
listWithAlias = makeListWithAlias(a)
}
)?.let { data ->
parseListP2(
CustomEmoji.decode,
accessInfo.apDomain,
accessInfo.apiHost,
data.decodeJsonArray()
)
}
val list = when {
accessInfo.isMastodon -> mastodonEmojis()
else -> misskeyEmojis12() ?: misskeyEmojis13()
}?.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.shortcode })
val listWithAlias = list?.let { srcList ->
ArrayList<CustomEmoji>(srcList).apply {
for (item in srcList) {
item.aliases
?.filter { !it.equals(item.shortcode, ignoreCase = true) }
?.forEach { add(item.makeAlias(it)) }
}
}
}?.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.alias ?: it.shortcode })
return synchronized(cache) {
val now = elapsedTime
if (list == null || listWithAlias == null) {
cacheError[cacheKey] = now
error("can't load custom emoji for ${accessInfo.apiHost}")
} else {
val mapShortCode = buildMap {
list.forEach { put(it.alias ?: it.shortcode, it) }
listWithAlias.forEach { put(it.alias ?: it.shortcode, it) }
}
var item = cache[cacheKey]
if (item == null) {
item = CacheItem(cacheKey, list, listWithAlias)
item = CacheItem(cacheKey, list, listWithAlias, mapShortCode)
cache[cacheKey] = item
} else {
item.list = list
item.listWithAliases = listWithAlias
item.mapShortCode = mapShortCode
item.timeUpdate = now
}
item
@ -249,39 +314,5 @@ class CustomEmojiLister(
if (++removed >= over) break
}
}
private fun decodeEmojiList(
data: String,
accessInfo: SavedAccount,
): List<CustomEmoji> =
if (accessInfo.isMisskey) {
parseList(
CustomEmoji.decodeMisskey,
accessInfo.apDomain,
data.decodeJsonObject().jsonArray("emojis")
)
} else {
parseList(
CustomEmoji.decode,
accessInfo.apDomain,
data.decodeJsonArray()
)
}.apply {
sortWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.shortcode })
}
private fun makeListWithAlias(
list: List<CustomEmoji>,
) = ArrayList<CustomEmoji>().apply {
addAll(list)
for (item in list) {
val aliases = item.aliases ?: continue
for (alias in aliases) {
if (alias.equals(item.shortcode, ignoreCase = true)) continue
add(item.makeAlias(alias))
}
}
sortWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.alias ?: it.shortcode })
}
}
}

View File

@ -1,10 +1,12 @@
package jp.juggler.subwaytooter.util
import android.content.Context
import android.os.SystemClock
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
@ -382,6 +384,11 @@ object EmojiDecoder {
// カスタム絵文字
val emojiCustom = emojiMapCustom?.get(name)
?: App1.custom_emoji_lister.getCachedEmoji(
options.linkHelper?.apiHost?.ascii,
name
)
if (emojiCustom != null) {
val url = when {
PrefB.bpDisableEmojiAnimation() && emojiCustom.staticUrl?.isNotEmpty() == true -> emojiCustom.staticUrl

View File

@ -161,4 +161,11 @@ class VersionString(src: String?) : Comparable<VersionString> {
}
}
}
// false if this is empty or argument is empty
// else, true is this is greater or equal argument version
fun ge(other: VersionString) = when {
this.isEmpty || other.isEmpty -> false
else -> this >= other
}
}

1
icon_material_symbols/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -0,0 +1,41 @@
plugins {
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
}
android {
namespace 'jp.juggler.icon_material_symbols'
compileSdk 32
defaultConfig {
minSdk 26
targetSdk 32
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.5.1'
implementation 'com.google.android.material:material:1.7.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.4'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0'
}

View File

View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,24 @@
package jp.juggler.icon_material_symbols
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("jp.juggler.icon_material_symbols.test", appContext.packageName)
}
}

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View File

@ -0,0 +1,17 @@
package jp.juggler.icon_material_symbols
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

View File

@ -1 +1,2 @@
include ':app', ':sample_apng', ':apng_android', ':apng', ':colorpicker', ':emoji'
include ':icon_material_symbols'