This commit is contained in:
tateisu 2024-03-18 00:55:04 +09:00
parent 6cd8839bbd
commit 50d0139a08
7 changed files with 442 additions and 2 deletions

View File

@ -25,8 +25,8 @@ android {
defaultConfig {
targetSdk = Vers.stTargetSdkVersion
minSdk = Vers.stMinSdkVersion
versionCode = 545
versionName = "5.545"
versionCode = 546
versionName = "5.546"
applicationId = "jp.juggler.subwaytooter"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

View File

@ -0,0 +1,63 @@
package jp.juggler.subwaytooter.ui.ossLicense
import android.app.Application
import android.content.Context
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.util.StColorScheme
import jp.juggler.util.coroutine.AppDispatchers
import jp.juggler.util.data.decodeJsonObject
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class ActOSSLicenseViewModel(
application: Application,
) : AndroidViewModel(application) {
val context: Context
get() = getApplication<Application>().applicationContext
private val loadError = Channel<Throwable>(capacity = Channel.CONFLATED)
private val _libraries = MutableStateFlow<List<LibText>>(emptyList())
val libraries = _libraries.asStateFlow()
private val _isProgressShown = MutableStateFlow(false)
val isProgressShown = _isProgressShown.asStateFlow()
fun load(stColorScheme: StColorScheme) = viewModelScope.launch {
try {
_isProgressShown.value = true
_libraries.value = withContext(AppDispatchers.IO) {
val root = context.resources.openRawResource(R.raw.dep_list)
.use { it.readBytes() }
.decodeToString()
.decodeJsonObject()
val licenses = root.jsonArray("licenses")
?.objectList()
?.associateBy { it.string("shortName") }
?: emptyMap()
root.jsonArray("libs")
?.objectList()
?.map {
parseLibText(
it,
licenses,
stColorScheme = stColorScheme,
)
}
?.sortedBy { it.nameSort }
?: emptyList()
}
} catch (ex: Throwable) {
if (ex is CancellationException) return@launch
loadError.send(ex)
} finally {
_isProgressShown.value = false
}
}
}

View File

@ -0,0 +1,92 @@
package jp.juggler.subwaytooter.ui.ossLicense
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.buildAnnotatedString
import jp.juggler.subwaytooter.util.StColorScheme
import jp.juggler.subwaytooter.util.annotateUrl
import jp.juggler.subwaytooter.util.isNotEmpty
import jp.juggler.subwaytooter.util.joinAnnotatedString
import jp.juggler.subwaytooter.util.toAnnotatedString
import jp.juggler.util.data.JsonObject
import jp.juggler.util.data.notEmpty
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
/**
* 依存ライブラリの装飾付きテキスト
*/
class LibText(
val nameBig: AnnotatedString,
val nameSmall: AnnotatedString?,
val desc: AnnotatedString,
) {
val nameSort = nameBig.toString().lowercase()
}
/**
* レシーバの文字列がカラでなければdstに追加する
* @receiver 追加先
* @param text 装飾付き文字列
* @param prefix nullや空でなければtextより先に追加される
*/
private fun AnnotatedString.Builder.appendLine(
text: CharSequence?,
prefix: CharSequence? = null,
) {
if (text.isNullOrBlank()) return
if (isNotEmpty()) append("\n")
if (!prefix.isNullOrBlank()) append(prefix)
append(text)
}
/**
* ライブラリ情報をLibTextに変換する
*/
fun parseLibText(
lib: JsonObject,
licenses: Map<String?, JsonObject>,
stColorScheme: StColorScheme,
): LibText {
val colorLink = stColorScheme.colorTextLink
val webSite = lib.string("website")?.toHttpUrlOrNull()?.toString()
val name = lib.string("name")?.notEmpty()
val id = lib.string("id")?.notEmpty()
val nameBig: CharSequence
val nameSmall: CharSequence?
if (name.isNullOrBlank()) {
// nameがない場合はnameBigはidを大きく表示する
nameBig = (id ?: "(no name, no id)").annotateUrl(webSite, colorLink)
nameSmall = null
} else {
nameBig = name.annotateUrl(webSite, colorLink)
nameSmall = id
// idがない場合はnameSmallはnullとなる
}
val licenseText = lib.jsonArray("licenses")?.stringList()
?.asSequence()
?.mapNotNull { licenses[it] }
?.map {
val uri =
it.jsonArray("urls")?.stringList()?.firstOrNull()?.toHttpUrlOrNull()?.toString()
it.string("name")!!.annotateUrl(uri, colorLink)
}
?.toList()?.joinAnnotatedString(AnnotatedString(", "))
val devNames = lib.jsonArray("developers")?.objectList()
?.map { it.string("name") }
?.filter { !it.isNullOrBlank() }
?.joinToString(", ")
val libDesc = buildAnnotatedString {
appendLine(lib.string("description")?.takeIf { it != name })
appendLine(devNames, "- Developers: ")
appendLine(licenseText, "- License: ")
}
return LibText(
nameBig = nameBig.toAnnotatedString(),
nameSmall = nameSmall?.toAnnotatedString(),
desc = libDesc.toAnnotatedString(),
)
}

View File

@ -0,0 +1,59 @@
package jp.juggler.subwaytooter.util
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.TextUnit
fun CharSequence.toAnnotatedString() = when (this) {
is AnnotatedString -> this
else -> AnnotatedString(toString())
}
fun AnnotatedString.Builder.isEmpty() = length == 0
fun AnnotatedString.Builder.isNotEmpty() = length != 0
/**
* 装飾付き文字列のリストを連結する
* @receiver 装飾付き文字列のリスト
* @param separator 文字列と文字列の間に挿入される区切り
*/
fun List<CharSequence>.joinAnnotatedString(
separator: CharSequence,
): AnnotatedString = AnnotatedString.Builder().also { dst ->
for (item in this) {
if (dst.isNotEmpty()) dst.append(separator)
dst.append(item)
}
}.toAnnotatedString()
/**
* クリック可能な装飾付き文字列を作る
* @receiver 表示文字列
* @param uri クリックしたら開くUrinullなら装飾しない
* @return CharSequence, 実際には Stringまたは AnnotatedString
*/
fun String.annotateUrl(
uri: String?,
colorLink: Color,
fontSize: TextUnit = TextUnit.Unspecified,
): CharSequence = when (uri) {
null -> this
else -> AnnotatedString.Builder().apply {
append(this@annotateUrl)
addStyle(
style = SpanStyle(
color = colorLink,
fontSize = fontSize,
textDecoration = TextDecoration.Underline
), start = 0, end = length
)
addStringAnnotation(
tag = "URL",
annotation = uri.toString(),
start = 0,
end = length,
)
}.toAnnotatedString()
}

View File

@ -0,0 +1,116 @@
package jp.juggler.subwaytooter.util
import android.app.Activity
import android.content.Context
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.pref.PrefI
import jp.juggler.util.ui.resColor
class StColorScheme(
val materialColorScheme: ColorScheme,
val colorTextLink: Color,
)
fun Context.createStColorSchemeLight(): StColorScheme {
val colorTextContent = Color(resColor(R.color.Light_colorTextContent))
val colorTextError = Color(resColor(R.color.Light_colorRegexFilterError))
val colorTextLink = Color(resColor(R.color.Light_colorLink))
return StColorScheme(
materialColorScheme = lightColorScheme(
error = colorTextError,
background = Color.White,
onBackground = colorTextContent,
primary = colorTextLink,
onPrimary = Color.White,
secondary = colorTextLink,
onSecondary = Color.White,
surface = Color(resColor(R.color.Light_colorColumnSettingBackground)),
onSurface = colorTextContent,
onTertiary = colorTextContent,
onSurfaceVariant = Color(resColor(R.color.Light_colorTextHint)),
),
colorTextLink = colorTextLink,
)
}
fun Context.createStColorSchemeDark(): StColorScheme {
val colorBackground = Color(resColor(R.color.Dark_colorBackground))
val colorTextContent = Color(resColor(R.color.Dark_colorTextContent))
val colorTextError = Color(resColor(R.color.Dark_colorRegexFilterError))
val colorTextLink = Color(resColor(R.color.Dark_colorLink))
return StColorScheme(
materialColorScheme = darkColorScheme(
error = colorTextError,
background = colorBackground,
onBackground = colorTextContent,
primary = colorTextLink,
onPrimary = colorTextContent,
secondary = colorTextLink,
onSecondary = colorTextContent,
surface = Color(resColor(R.color.Dark_colorColumnSettingBackground)),
onSurface = colorTextContent,
onTertiary = colorTextContent,
onSurfaceVariant = Color(resColor(R.color.Dark_colorTextHint)),
),
colorTextLink = colorTextLink,
)
}
fun Context.createStColorSchemeMastodonDark(): StColorScheme {
val colorBackground = Color(resColor(R.color.Mastodon_colorBackground))
val colorTextContent = Color(resColor(R.color.Mastodon_colorTextContent))
val colorTextError = Color(resColor(R.color.Mastodon_colorRegexFilterError))
val colorTextLink = Color(resColor(R.color.Mastodon_colorLink))
return StColorScheme(
materialColorScheme = darkColorScheme(
error = colorTextError,
background = colorBackground,
onBackground = colorTextContent,
primary = Color(resColor(R.color.Mastodon_colorAppCompatAccent)),
onPrimary = colorTextContent,
secondary = colorTextLink,
onSecondary = colorTextContent,
surface = Color(resColor(R.color.Mastodon_colorColumnSettingBackground)),
onSurface = colorTextContent,
onTertiary = colorTextContent,
onSurfaceVariant = Color(resColor(R.color.Mastodon_colorTextHint)),
),
colorTextLink = colorTextLink,
)
}
fun Activity.getStColorTheme(forceDark: Boolean = false): StColorScheme {
App1.prepare(applicationContext, "getStColorTheme")
var nTheme = PrefI.ipUiTheme.value
if (forceDark && nTheme == 0) nTheme = 1
return when (nTheme) {
2 -> createStColorSchemeMastodonDark()
1 -> createStColorSchemeDark()
else -> createStColorSchemeLight()
}
}
fun dummyStColorTheme() = StColorScheme(
materialColorScheme = darkColorScheme(),
colorTextLink = Color.Cyan,
)

View File

@ -0,0 +1,63 @@
package jp.juggler.subwaytooter.util
import androidx.activity.ComponentActivity
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
// ViewModelのfactoryを毎回書くのが面倒
// あと使わない場合にはViewModelの引数を生成したくない
fun <VM : ViewModel> viewModelFactory(vmClass: Class<VM>, creator: () -> VM) =
object : ViewModelProvider.NewInstanceFactory() {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (!modelClass.isAssignableFrom(vmClass)) {
error("unexpected modelClass. ${modelClass.simpleName}")
}
return creator() as T
}
}
// ViewModelProvider(…).get を毎回書くのが面倒
inline fun <reified T : ViewModel> provideViewModel(
owner: ViewModelStoreOwner,
noinline creator: () -> T,
) = ViewModelProvider(owner, viewModelFactory(T::class.java, creator))[T::class.java]
inline fun <reified T : ViewModel> provideViewModel(
owner: ViewModelStoreOwner,
key: String,
noinline creator: () -> T,
) = ViewModelProvider(owner, viewModelFactory(T::class.java, creator))[key, T::class.java]
fun <T : Any?> AppCompatActivity.collectOnLifeCycle(
flow: Flow<T>,
state: Lifecycle.State = Lifecycle.State.STARTED,
block: suspend (T) -> Unit,
) = lifecycleScope.launch {
lifecycle.repeatOnLifecycle(state = state) {
flow.collect {
block(it)
// Viewの更新
}
}
}
fun <T : Any?> ComponentActivity.collectOnLifeCycle(
flow: Flow<T>,
state: Lifecycle.State = Lifecycle.State.STARTED,
block: suspend (T) -> Unit,
) = lifecycleScope.launch {
lifecycle.repeatOnLifecycle(state = state) {
flow.collect {
block(it)
// Viewの更新
}
}
}

View File

@ -0,0 +1,47 @@
package com.jrummyapps.android.colorpicker
import android.content.Context
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.os.Parcelable
import android.util.TypedValue
internal fun Context.dpToPx(dipValue: Float): Int {
val metrics = resources.displayMetrics
val v = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dipValue, metrics)
val res = (v + 0.5).toInt() // Round
// Ensure at least 1 pixel if val was > 0
return if (res == 0 && v > 0) 1 else res
}
internal fun Context.dpToPx(dipValue: Int): Int =
dpToPx(dipValue.toFloat())
internal inline fun <reified T : Parcelable> Bundle.getParcelableCompat(key: String) =
if (Build.VERSION.SDK_INT >= 33) {
getParcelable(key, T::class.java)
} else {
@Suppress("DEPRECATION")
getParcelable(key)
}
// 単体テストするのでpublic
fun String.parseColor(): Int {
val start = if (startsWith("#")) 1 else 0
fun c1(offset: Int) = substring(start + offset, start + offset + 1).toInt(16) * 0x11
fun c2(offset: Int) = substring(start + offset, start + offset + 2).toInt(16)
return when (length - start) {
0 -> Color.BLACK
1 -> Color.argb(255, c1(0), c1(0), c1(0))
2 -> Color.argb(255, c1(0), c1(1), 0x80)
3 -> Color.argb(255, c1(0), c1(1), c1(2))
4 -> Color.argb(c1(0), c1(1), c1(2), c1(3))
5 -> Color.argb(255, c2(0), c2(2), c1(4))
6 -> Color.argb(255, c2(0), c2(2), c2(4))
7 -> Color.argb(c2(0), c2(2), c2(4), c1(6))
8 -> Color.argb(c2(0), c2(2), c2(4), c2(6))
else -> Color.WHITE
}
}