198 lines
6.0 KiB
Kotlin
198 lines
6.0 KiB
Kotlin
package jp.juggler.subwaytooter.util
|
|
|
|
import android.graphics.Color
|
|
import android.graphics.drawable.Drawable
|
|
import android.graphics.drawable.GradientDrawable
|
|
import android.os.SystemClock
|
|
import jp.juggler.subwaytooter.App1
|
|
import jp.juggler.util.*
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.GlobalScope
|
|
import kotlinx.coroutines.channels.Channel
|
|
import kotlinx.coroutines.launch
|
|
import java.util.concurrent.ConcurrentHashMap
|
|
import java.util.regex.Pattern
|
|
|
|
object OpenSticker {
|
|
|
|
private val log = LogCategory("OpenSticker")
|
|
|
|
private const val alnum = """[0-9a-fA-F]"""
|
|
|
|
private const val colorFgDefault = Color.WHITE
|
|
|
|
private val reColor6 = """#($alnum{2})($alnum{2})($alnum{2})"""
|
|
.asciiPattern(Pattern.CASE_INSENSITIVE)
|
|
|
|
private val reColor3 = """#($alnum)($alnum)($alnum)\b"""
|
|
.asciiPattern(Pattern.CASE_INSENSITIVE)
|
|
|
|
private fun parseHex(group: String?): Int = group?.toInt(16) ?: 0
|
|
|
|
private fun String.parseColor(): Int? {
|
|
reColor6.matcher(this).findOrNull()?.let {
|
|
return Color.rgb(
|
|
parseHex(it.groupEx(1)),
|
|
parseHex(it.groupEx(2)),
|
|
parseHex(it.groupEx(3))
|
|
)
|
|
}
|
|
reColor3.matcher(this).findOrNull()?.let {
|
|
return Color.rgb(
|
|
parseHex(it.groupEx(1)) * 0x11,
|
|
parseHex(it.groupEx(2)) * 0x11,
|
|
parseHex(it.groupEx(3)) * 0x11
|
|
)
|
|
}
|
|
if (isNotEmpty()) log.e("parseColor: can't parse $this")
|
|
return null
|
|
}
|
|
|
|
private fun String.toColor(): Int = parseColor() ?: error("not a color: $this")
|
|
|
|
class ColorBg(val array: IntArray) {
|
|
companion object {
|
|
|
|
val map = HashMap<String, GradientDrawable>()
|
|
}
|
|
|
|
constructor(src: List<Int>) : this(
|
|
IntArray(src.size + 1) {
|
|
if (it == 0) Color.TRANSPARENT else src[it - 1]
|
|
}
|
|
)
|
|
|
|
constructor(src: Int) : this(
|
|
IntArray(2) {
|
|
if (it == 0) Color.TRANSPARENT else src
|
|
}
|
|
)
|
|
|
|
val key = array.joinToString(",") { it.toString() }
|
|
|
|
val size = array.size
|
|
|
|
fun first() = array.first()
|
|
fun last() = array.last()
|
|
|
|
fun getGradation(): Drawable {
|
|
var v = map[key]
|
|
if (v == null) {
|
|
v = GradientDrawable(GradientDrawable.Orientation.RIGHT_LEFT, array)
|
|
map[key] = v
|
|
}
|
|
return v
|
|
}
|
|
}
|
|
|
|
class Default(
|
|
val fontColor: Int,
|
|
val bgColor: ColorBg
|
|
)
|
|
|
|
private val colorBgDefault = ColorBg("#27c".toColor())
|
|
|
|
private fun JsonArray.parseBgColor() =
|
|
mapNotNull { it.cast<String>()?.parseColor() }
|
|
.takeIf { it.isNotEmpty() }
|
|
?.let { ColorBg(it) }
|
|
|
|
class Item(src: JsonObject, defaults: Map<String, Default>) {
|
|
|
|
// domain name such as "m.aqr.af"
|
|
// ホスト名とAPドメイン名が異なるケースは想定されていない
|
|
val domain = src.string("domain")!!
|
|
|
|
// display name such as "まくらふ丼"
|
|
val name = src.string("name") ?: domain
|
|
|
|
// 'mastodon'|'pleroma'|'misskey'|'misskeylegacy'|'pixelfed' //misskeyはv12でなければlegacy
|
|
val type = src.string("type")!!
|
|
|
|
//nullならIDefault[type].bgColorを参照
|
|
val bgColor = src.jsonArray("bgColor")?.parseBgColor()
|
|
?: defaults[type]?.bgColor
|
|
?: colorBgDefault
|
|
|
|
//nullならIDefault[type].fontColorを参照
|
|
val fontColor = src.string("fontColor")?.parseColor()
|
|
?: defaults[type]?.fontColor
|
|
?: colorFgDefault
|
|
|
|
//普通はこっちを読んでください PNG/15px, 15px
|
|
val favicon = src.string("favicon")!!
|
|
|
|
// [6]画像の横幅
|
|
val imageWidth: Int = 15
|
|
}
|
|
|
|
/////////////////////////////////////
|
|
|
|
private class RequestItem {
|
|
val result = Channel<Unit>()
|
|
}
|
|
|
|
private var timeNextLoad = 0L
|
|
|
|
var lastList = ConcurrentHashMap<String, Item>()
|
|
|
|
private suspend fun loadOne() {
|
|
|
|
// 頻繁に読み直さない
|
|
val now = SystemClock.elapsedRealtime()
|
|
if (timeNextLoad - now > 0) return
|
|
timeNextLoad = now + 301000L
|
|
|
|
val text = App1.getHttpCachedString("https://s.0px.io/json")
|
|
if (text?.isEmpty() != false) return
|
|
|
|
val root = text.decodeJsonObject()
|
|
log.d("OpenSticker: updated=${root.string("updated")}")
|
|
|
|
// read defaults
|
|
val defaults = HashMap<String, Default>()
|
|
|
|
for (entry in root.jsonObject("default") ?: JsonObject()) {
|
|
val key = entry.key
|
|
val value = entry.value.cast<JsonObject>() ?: continue
|
|
val fontColor = value.string("fontColor")?.parseColor()
|
|
val bgColor = value.jsonArray("bgColor")?.parseBgColor()
|
|
if (fontColor != null && bgColor != null)
|
|
defaults[key] = Default(fontColor, bgColor)
|
|
}
|
|
|
|
val list = ConcurrentHashMap<String, Item>()
|
|
for (src in root.jsonArray("data") ?: JsonArray()) {
|
|
try {
|
|
if (src is JsonObject) {
|
|
val item = Item(src, defaults)
|
|
list[item.domain] = item
|
|
}
|
|
} catch (ex: Throwable) {
|
|
log.e(ex, "parse failed.")
|
|
}
|
|
}
|
|
if (list.isNotEmpty()) lastList = list
|
|
}
|
|
|
|
private val requestQueue = Channel<RequestItem>(capacity = Channel.UNLIMITED)
|
|
|
|
// キューにリクエストを送った後、それが消化されるまで待つ
|
|
suspend fun loadAndWait() =
|
|
RequestItem()
|
|
.also { requestQueue.send(it) }
|
|
.result.receive()
|
|
|
|
init {
|
|
// リクエストを処理するコルーチン。プロセスが止まるまでキャンセルされない
|
|
GlobalScope.launch(Dispatchers.Default) {
|
|
while (true) {
|
|
val item = requestQueue.receive()
|
|
runCatching{loadOne()}
|
|
.onFailure { log.e(it, "load failed.") }
|
|
item.result.send(Unit)
|
|
}
|
|
}
|
|
}
|
|
}
|