v5.546
This commit is contained in:
parent
6cd8839bbd
commit
50d0139a08
|
@ -25,8 +25,8 @@ android {
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
targetSdk = Vers.stTargetSdkVersion
|
targetSdk = Vers.stTargetSdkVersion
|
||||||
minSdk = Vers.stMinSdkVersion
|
minSdk = Vers.stMinSdkVersion
|
||||||
versionCode = 545
|
versionCode = 546
|
||||||
versionName = "5.545"
|
versionName = "5.546"
|
||||||
applicationId = "jp.juggler.subwaytooter"
|
applicationId = "jp.juggler.subwaytooter"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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(),
|
||||||
|
)
|
||||||
|
}
|
|
@ -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 クリックしたら開くUri。nullなら装飾しない
|
||||||
|
* @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()
|
||||||
|
}
|
|
@ -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,
|
||||||
|
)
|
|
@ -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の更新
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue