v5.546
This commit is contained in:
parent
6cd8839bbd
commit
50d0139a08
|
@ -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"
|
||||
|
|
|
@ -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