(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.app" target="11" />
<module name="SubwayTooter.colorpicker" target="11" /> <module name="SubwayTooter.colorpicker" target="11" />
<module name="SubwayTooter.emoji" target="11" /> <module name="SubwayTooter.emoji" target="11" />
<module name="SubwayTooter.icon_material_symbols" target="11" />
<module name="SubwayTooter.sample_apng" target="11" /> <module name="SubwayTooter.sample_apng" target="11" />
</bytecodeTargetLevel> </bytecodeTargetLevel>
</component> </component>

View File

@ -16,6 +16,7 @@
<option value="$PROJECT_DIR$/app" /> <option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/colorpicker" /> <option value="$PROJECT_DIR$/colorpicker" />
<option value="$PROJECT_DIR$/emoji" /> <option value="$PROJECT_DIR$/emoji" />
<option value="$PROJECT_DIR$/icon_material_symbols" />
<option value="$PROJECT_DIR$/sample_apng" /> <option value="$PROJECT_DIR$/sample_apng" />
</set> </set>
</option> </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.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.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/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.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.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" /> <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 package jp.juggler.subwaytooter
import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.DownloadManager import android.app.DownloadManager
import android.content.ClipData import android.content.ClipData
@ -296,6 +295,7 @@ class ActMediaViewer : AppCompatActivity(), View.OnClickListener {
App1.initEdgeToEdge(this) App1.initEdgeToEdge(this)
views.pbvImage.background = MediaBackgroundDrawable( views.pbvImage.background = MediaBackgroundDrawable(
context = views.root.context,
tileStep = tileStep, tileStep = tileStep,
kind = MediaBackgroundDrawable.Kind.fromIndex(PrefI.ipMediaBackground(this)) kind = MediaBackgroundDrawable.Kind.fromIndex(PrefI.ipMediaBackground(this))
) )
@ -811,11 +811,12 @@ class ActMediaViewer : AppCompatActivity(), View.OnClickListener {
private fun mediaBackgroundDialog() { private fun mediaBackgroundDialog() {
val ad = ActionsDialog() val ad = ActionsDialog()
for (k in MediaBackgroundDrawable.Kind.values()) { for (k in MediaBackgroundDrawable.Kind.values()) {
if (!k.isMediaBackground) continue
ad.addAction(k.name) { ad.addAction(k.name) {
val idx = k.toIndex() val idx = k.toIndex()
appPref.edit().put(PrefI.ipMediaBackground, idx).apply() appPref.edit().put(PrefI.ipMediaBackground, idx).apply()
views.pbvImage.background = MediaBackgroundDrawable( views.pbvImage.background = MediaBackgroundDrawable(
context = views.root.context,
tileStep = tileStep, tileStep = tileStep,
kind = k kind = k
) )

View File

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

View File

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

View File

@ -5,7 +5,6 @@ import jp.juggler.util.JsonArray
import jp.juggler.util.JsonException import jp.juggler.util.JsonException
import jp.juggler.util.JsonObject import jp.juggler.util.JsonObject
import jp.juggler.util.LogCategory import jp.juggler.util.LogCategory
import java.util.HashMap
object EntityUtil { object EntityUtil {
val log = LogCategory("EntityUtil") val log = LogCategory("EntityUtil")
@ -122,8 +121,9 @@ inline fun <reified K, reified V> parseMapOrNull(
} }
inline fun <reified K, reified V> parseMapOrNull( inline fun <reified K, reified V> parseMapOrNull(
factory: (host: Host, src: JsonObject) -> V, factory: (apDomain: Host, apiHost: Host, src: JsonObject) -> V,
host: Host, apDomain: Host,
apiHost: Host,
src: JsonArray?, src: JsonArray?,
log: LogCategory = EntityUtil.log, log: LogCategory = EntityUtil.log,
): HashMap<K, V>? where V : Mappable<K> { ): HashMap<K, V>? where V : Mappable<K> {
@ -132,7 +132,7 @@ inline fun <reified K, reified V> parseMapOrNull(
if (size > 0) { if (size > 0) {
val dst = HashMap<K, V>() val dst = HashMap<K, V>()
for (i in 0 until size) { 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 (item != null) dst[item.mapKey] = item
} }
if (dst.isNotEmpty()) return dst 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( inline fun <reified T> parseItem(
factory: (serviceType: ServiceType, src: JsonObject) -> T, factory: (serviceType: ServiceType, src: JsonObject) -> T,
serviceType: ServiceType, serviceType: ServiceType,
@ -198,9 +214,9 @@ inline fun <reified T> parseItem(
} }
} }
inline fun <reified T> parseList( inline fun <P1, reified T> parseListP1(
factory: (parser: TootParser, src: JsonObject) -> T, factory: (p1: P1, src: JsonObject) -> T,
parser: TootParser, p1: P1,
src: JsonArray?, src: JsonArray?,
log: LogCategory = EntityUtil.log, log: LogCategory = EntityUtil.log,
): ArrayList<T> { ): ArrayList<T> {
@ -210,7 +226,28 @@ inline fun <reified T> parseList(
if (src_length > 0) { if (src_length > 0) {
dst.ensureCapacity(src_length) dst.ensureCapacity(src_length)
for (i in src.indices) { 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) 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.JsonObject
import jp.juggler.util.LogCategory import jp.juggler.util.LogCategory
class MisskeyNoteUpdate(apDomain: Host, src: JsonObject) { class MisskeyNoteUpdate(apDomain: Host, apiHost: Host, src: JsonObject) {
companion object { companion object {
private val log = LogCategory("MisskeyNoteUpdate") private val log = LogCategory("MisskeyNoteUpdate")
} }
@ -37,7 +37,7 @@ class MisskeyNoteUpdate(apDomain: Host, src: JsonObject) {
userId = EntityId.mayDefault(body2.string("userId")) userId = EntityId.mayDefault(body2.string("userId"))
emoji = body2.jsonObject("emoji")?.let { emoji = body2.jsonObject("emoji")?.let {
try { try {
CustomEmoji.decodeMisskey(apDomain, it) CustomEmoji.decodeMisskey(apDomain, apiHost, it)
} catch (ex: Throwable) { } catch (ex: Throwable) {
log.e(ex, "can't parse custom emoji.") log.e(ex, "can't parse custom emoji.")
null null

View File

@ -138,6 +138,7 @@ open class TootAccount(parser: TootParser, src: JsonObject) : HostAndDomain {
parseMapOrNull( parseMapOrNull(
CustomEmoji.decodeMisskey, CustomEmoji.decodeMisskey,
parser.apDomain, parser.apDomain,
parser.apiHost,
src.jsonArray("emojis") src.jsonArray("emojis")
) )
this.profile_emojis = null this.profile_emojis = null
@ -276,8 +277,12 @@ open class TootAccount(parser: TootParser, src: JsonObject) : HostAndDomain {
else -> { else -> {
// 絵文字データは先に読んでおく // 絵文字データは先に読んでおく
this.custom_emojis = this.custom_emojis = parseMapOrNull(
parseMapOrNull(CustomEmoji.decode, parser.apDomain, src.jsonArray("emojis")) CustomEmoji.decode,
parser.apDomain,
parser.apiHost,
src.jsonArray("emojis")
)
this.profile_emojis = when (val o = src["profile_emojis"]) { this.profile_emojis = when (val o = src["profile_emojis"]) {
is JsonArray -> parseMapOrNull(::NicoProfileEmoji, o, TootStatus.log) 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.JsonObject
import jp.juggler.util.LogCategory import jp.juggler.util.LogCategory
import jp.juggler.util.notEmpty import jp.juggler.util.notEmpty
import java.util.*
class TootAnnouncement(parser: TootParser, src: JsonObject) { class TootAnnouncement(parser: TootParser, src: JsonObject) {
@ -44,8 +43,13 @@ class TootAnnouncement(parser: TootParser, src: JsonObject) {
init { init {
// 絵文字マップはすぐ後で使うので、最初の方で読んでおく // 絵文字マップはすぐ後で使うので、最初の方で読んでおく
this.custom_emojis = this.custom_emojis = parseMapOrNull(
parseMapOrNull(CustomEmoji.decode, parser.apDomain, src.jsonArray("emojis"), log) CustomEmoji.decode,
parser.apDomain,
parser.apiHost,
src.jsonArray("emojis"),
log
)
this.tags = TootTag.parseListOrNull(parser, src.jsonArray("tags")) this.tags = TootTag.parseListOrNull(parser, src.jsonArray("tags"))

View File

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

View File

@ -238,13 +238,13 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() {
// お気に入りカラムなどではパース直後に変更することがある // お気に入りカラムなどではパース直後に変更することがある
// 絵文字マップはすぐ後で使うので、最初の方で読んでおく // 絵文字マップはすぐ後で使うので、最初の方で読んでおく
this.custom_emojis = this.custom_emojis = parseMapOrNull(
parseMapOrNull( CustomEmoji.decodeMisskey,
CustomEmoji.decodeMisskey, parser.apDomain,
parser.apDomain, parser.apiHost,
src.jsonArray("emojis"), src.jsonArray("emojis"),
log log
) )
this.profile_emojis = null this.profile_emojis = null
val who = parser.account(src.jsonObject("user")) 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.created_at = src.string("created_at")
// 絵文字マップはすぐ後で使うので、最初の方で読んでおく // 絵文字マップはすぐ後で使うので、最初の方で読んでおく
this.custom_emojis = this.custom_emojis = parseMapOrNull(
parseMapOrNull( CustomEmoji.decode,
CustomEmoji.decode, parser.apDomain,
parser.apDomain, parser.apiHost,
src.jsonArray("emojis"), src.jsonArray("emojis"),
log log
) )
this.profile_emojis = when (val o = src["profile_emojis"]) { this.profile_emojis = when (val o = src["profile_emojis"]) {
is JsonArray -> parseMapOrNull(::NicoProfileEmoji, o, log) is JsonArray -> parseMapOrNull(::NicoProfileEmoji, o, log)
@ -809,9 +809,9 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() {
} }
private fun mergeMentions( private fun mergeMentions(
mentions1: java.util.ArrayList<TootMention>?, mentions1: List<TootMention>?,
mentions2: java.util.ArrayList<TootMention>?, mentions2: List<TootMention>?,
): java.util.ArrayList<TootMention>? { ): ArrayList<TootMention>? {
val size = (mentions1?.size ?: 0) + (mentions2?.size ?: 0) val size = (mentions1?.size ?: 0) + (mentions2?.size ?: 0)
if (size == 0) return null if (size == 0) return null
val dst = ArrayList<TootMention>(size) 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) sw(PrefB.bpUseInternalMediaViewer, R.string.use_internal_media_viewer)
spinner(PrefI.ipMediaBackground, R.string.background_pattern) { 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) 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.App1
import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.databinding.EmojiPickerDialogBinding import jp.juggler.subwaytooter.databinding.EmojiPickerDialogBinding
import jp.juggler.subwaytooter.drawable.MediaBackgroundDrawable
import jp.juggler.subwaytooter.emoji.* import jp.juggler.subwaytooter.emoji.*
import jp.juggler.subwaytooter.global.appPref import jp.juggler.subwaytooter.global.appPref
import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.subwaytooter.pref.PrefB
@ -574,7 +575,7 @@ private class EmojiPicker(
category.createFiltered(keywordLower) category.createFiltered(keywordLower)
.takeIf { it.items.isNotEmpty() } .takeIf { it.items.isNotEmpty() }
}.forEach { }.forEach {
if (selectedCategory == null) add(it) if( it.category == EmojiCategory.Custom || selectedCategory == null) add(it)
addAll(it.items) addAll(it.items)
val mod = it.items.size % gridCols val mod = it.items.size % gridCols
if (mod > 0) { if (mod > 0) {
@ -743,8 +744,12 @@ private class EmojiPicker(
views.etFilter.addTextChangedListener(textWatcher) views.etFilter.addTextChangedListener(textWatcher)
showFiltered(null, null) showFiltered(null, null)
val density = activity.resources.displayMetrics.density
views.giGrid.intercept = { handleTouch(it, wasIntercept = false) } views.giGrid.intercept = { handleTouch(it, wasIntercept = false) }
views.giGrid.touch = { handleTouch(it, wasIntercept = true) } views.giGrid.touch = { handleTouch(it, wasIntercept = true) }

View File

@ -1,22 +1,35 @@
package jp.juggler.subwaytooter.drawable package jp.juggler.subwaytooter.drawable
import android.content.Context
import android.graphics.* import android.graphics.*
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import androidx.annotation.ColorInt import jp.juggler.subwaytooter.R
import jp.juggler.util.attrColor
import kotlin.math.min import kotlin.math.min
class MediaBackgroundDrawable( class MediaBackgroundDrawable(
private val context: Context,
private val tileStep: Int, private val tileStep: Int,
private val kind: Kind private val kind: Kind,
) : Drawable() { ) : Drawable() {
enum class Kind(@ColorInt val c1: Int, @ColorInt val c2: Int = 0) { enum class Kind(
Black(Color.BLACK), val c1: Context.() -> Int,
BlackTile(Color.BLACK, Color.BLACK or 0x202020), val c2: Context.() -> Int,
Grey(Color.BLACK or 0x787878), val isMediaBackground: Boolean = true,
GreyTile(Color.BLACK or 0x707070, Color.BLACK or 0x808080), ) {
White(Color.WHITE), Black({ Color.BLACK }, { 0 }),
WhiteTile(Color.WHITE, Color.BLACK or 0xe0e0e0), 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) { override fun draw(canvas: Canvas) {
val bounds = this.bounds val bounds = this.bounds
val c1 = kind.c1 val c1 = kind.c1.invoke(context)
val c2 = kind.c2 val c2 = kind.c2.invoke(context)
if (c2 == 0) { if (c2 == 0) {
paint.color = c1 paint.color = c1
canvas.drawRect(bounds, paint) 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.JsonArray
import jp.juggler.util.JsonObject import jp.juggler.util.JsonObject
import jp.juggler.util.notEmpty import jp.juggler.util.notEmpty
import java.util.*
sealed interface EmojiBase sealed interface EmojiBase
@ -81,7 +80,7 @@ class CustomEmoji(
companion object { companion object {
val decode: (Host, JsonObject) -> CustomEmoji = { apDomain, src -> val decode: (Host, Host, JsonObject) -> CustomEmoji = { apDomain, _, src ->
CustomEmoji( CustomEmoji(
apDomain = apDomain, apDomain = apDomain,
shortcode = src.stringOrThrow("shortcode"), 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") val url = src.string("url") ?: error("missing url")
CustomEmoji( 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>? { private fun parseAliases(src: JsonArray?): ArrayList<String>? {
var dst = null as ArrayList<String>? var dst = null as ArrayList<String>?
if (src != null) { if (src != null) {

View File

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

View File

@ -4,7 +4,7 @@ import android.content.Context
import android.os.Handler import android.os.Handler
import android.os.SystemClock import android.os.SystemClock
import jp.juggler.subwaytooter.App1 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.emoji.CustomEmoji
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.* import jp.juggler.util.*
@ -35,6 +35,7 @@ class CustomEmojiLister(
val key: String, val key: String,
var list: List<CustomEmoji>, var list: List<CustomEmoji>,
var listWithAliases: List<CustomEmoji>, var listWithAliases: List<CustomEmoji>,
var mapShortCode: Map<String, CustomEmoji>,
// ロードした時刻 // ロードした時刻
var timeUpdate: Long = elapsedTime, var timeUpdate: Long = elapsedTime,
// 参照された時刻 // 参照された時刻
@ -63,7 +64,12 @@ class CustomEmojiLister(
// エラーキャッシュ // エラーキャッシュ
internal val cacheError = ConcurrentHashMap<String, Long>() 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>() internal val queue = ConcurrentLinkedQueue<Request>()
@ -75,18 +81,21 @@ class CustomEmojiLister(
cacheError.clear() cacheError.clear()
} }
private fun getCached(now: Long, accessInfo: SavedAccount): CacheItem? { private fun getCached(now: Long, accessInfo: SavedAccount) =
val host = accessInfo.apiHost.ascii 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) { if (item != null && now - item.timeUpdate <= ERROR_EXPIRE) {
item.timeUsed = now item.timeUsed = now
return item return item
} }
// エラーキャッシュ // エラーキャッシュ
val timeError = cacheError[host] val timeError = cacheError[apiHostAscii]
if (timeError != null && now < timeError + ERROR_EXPIRE) { if (timeError != null && now < timeError + ERROR_EXPIRE) {
return cacheErrorItem 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() { private inner class Worker : WorkerBase() {
override fun cancel() { override fun cancel() {
@ -186,39 +198,92 @@ class CustomEmojiLister(
val accessInfo = request.accessInfo val accessInfo = request.accessInfo
val cacheKey = accessInfo.apiHost.ascii val cacheKey = accessInfo.apiHost.ascii
val data = if (accessInfo.isMisskey) {
// v12のmetaからemojisをパース
suspend fun misskeyEmojis12(): List<CustomEmoji>? =
App1.getHttpCachedString( App1.getHttpCachedString(
"https://$cacheKey/api/meta", "https://$cacheKey/api/meta",
accessInfo = accessInfo accessInfo = accessInfo
) { builder -> ) { builder ->
builder.post(JsonObject().toRequestBody()) 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( App1.getHttpCachedString(
"https://$cacheKey/api/v1/custom_emojis", "https://$cacheKey/api/v1/custom_emojis",
accessInfo = accessInfo accessInfo = accessInfo
) )?.let { data ->
} parseListP2(
var list: List<CustomEmoji>? = null CustomEmoji.decode,
var listWithAlias: List<CustomEmoji>? = null accessInfo.apDomain,
if (data != null) { accessInfo.apiHost,
val a = decodeEmojiList(data, accessInfo) data.decodeJsonArray()
list = a )
listWithAlias = makeListWithAlias(a) }
}
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) { return synchronized(cache) {
val now = elapsedTime val now = elapsedTime
if (list == null || listWithAlias == null) { if (list == null || listWithAlias == null) {
cacheError[cacheKey] = now cacheError[cacheKey] = now
error("can't load custom emoji for ${accessInfo.apiHost}") error("can't load custom emoji for ${accessInfo.apiHost}")
} else { } else {
val mapShortCode = buildMap {
list.forEach { put(it.alias ?: it.shortcode, it) }
listWithAlias.forEach { put(it.alias ?: it.shortcode, it) }
}
var item = cache[cacheKey] var item = cache[cacheKey]
if (item == null) { if (item == null) {
item = CacheItem(cacheKey, list, listWithAlias) item = CacheItem(cacheKey, list, listWithAlias, mapShortCode)
cache[cacheKey] = item cache[cacheKey] = item
} else { } else {
item.list = list item.list = list
item.listWithAliases = listWithAlias item.listWithAliases = listWithAlias
item.mapShortCode = mapShortCode
item.timeUpdate = now item.timeUpdate = now
} }
item item
@ -249,39 +314,5 @@ class CustomEmojiLister(
if (++removed >= over) break 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 package jp.juggler.subwaytooter.util
import android.content.Context import android.content.Context
import android.os.SystemClock
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.Spanned import android.text.Spanned
import android.util.SparseBooleanArray import android.util.SparseBooleanArray
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.emoji.CustomEmoji import jp.juggler.subwaytooter.emoji.CustomEmoji
import jp.juggler.subwaytooter.emoji.EmojiMap import jp.juggler.subwaytooter.emoji.EmojiMap
@ -382,6 +384,11 @@ object EmojiDecoder {
// カスタム絵文字 // カスタム絵文字
val emojiCustom = emojiMapCustom?.get(name) val emojiCustom = emojiMapCustom?.get(name)
?: App1.custom_emoji_lister.getCachedEmoji(
options.linkHelper?.apiHost?.ascii,
name
)
if (emojiCustom != null) { if (emojiCustom != null) {
val url = when { val url = when {
PrefB.bpDisableEmojiAnimation() && emojiCustom.staticUrl?.isNotEmpty() == true -> emojiCustom.staticUrl 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 ':app', ':sample_apng', ':apng_android', ':apng', ':colorpicker', ':emoji'
include ':icon_material_symbols'