peachsim 0.1.674 → 0.1.675
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract-input.cjs +1 -1
- package/dist-lib/bridge-contract-input.mjs +1 -1
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +1 -1
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/cloud-contract.cjs +1 -1
- package/dist-lib/cloud-contract.mjs +1 -1
- package/dist-lib/cloud-test-args.cjs +1 -1
- package/dist-lib/cloud-test-args.mjs +1 -1
- package/dist-lib/cloud.cjs +1 -1
- package/dist-lib/cloud.mjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +1 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +1 -1
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/replacement-module-handler.cjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +1 -1
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +1 -1
- package/dist-lib/kotlin-host.cjs +78 -2
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro-fingerprint-registry.cjs +1 -1
- package/dist-lib/metro-fingerprint-registry.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +1 -1
- package/dist-lib/metro-production-bundle.mjs +1 -1
- package/dist-lib/metro.cjs +1 -1
- package/dist-lib/native-app-identity.cjs +1 -1
- package/dist-lib/native-app-identity.mjs +1 -1
- package/dist-lib/native-firebase-metadata.cjs +1 -1
- package/dist-lib/native-firebase-metadata.mjs +1 -1
- package/dist-lib/plist.cjs +1 -1
- package/dist-lib/plist.mjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +1 -1
- package/dist-lib/sdk.mjs +1 -1
- package/dist-lib/skills.cjs +1 -1
- package/dist-lib/swift-compile.cjs +1 -1
- package/dist-lib/swift-compile.mjs +1 -1
- package/dist-lib/swift-package.cjs +1 -1
- package/dist-lib/swift-package.mjs +1 -1
- package/dist-lib/swift-submit.cjs +1 -1
- package/dist-lib/swift-submit.mjs +1 -1
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +1 -1
- package/dist-lib/vite.cjs +1 -1
- package/kotlin/AppSources/android/content/Context.kt +4 -0
- package/kotlin/AppSources/android/content/res/Configuration.kt +4 -1
- package/kotlin/AppSources/android/content/res/Resources.kt +45 -1
- package/kotlin/AppSources/androidx/compose/ui/platform/AndroidCompositionLocals.kt +19 -0
- package/kotlin/AppSources/androidx/compose/ui/res/StringResources.kt +53 -0
- package/kotlin/AppSources/dev/peach/app/JavaFormat.kt +264 -0
- package/kotlin/AppSources/dev/peach/app/PeachApp.kt +23 -7
- package/kotlin/fixtures/kotlin-app-variants/Strings.kt +49 -0
- package/package.json +1 -1
- package/src/android-strings.ts +163 -0
- package/src/android-xml.ts +3 -3
- package/src/kotlin-app.ts +4 -0
- package/src/kotlin-host.ts +98 -0
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
package android.content.res
|
|
2
2
|
|
|
3
3
|
// the device configuration an app reads through its resources: a phone's
|
|
4
|
-
// normal ui mode type, night or not as the device's dark theme is set
|
|
4
|
+
// normal ui mode type, night or not as the device's dark theme is set, and
|
|
5
|
+
// the device locale's language tag
|
|
5
6
|
class Configuration {
|
|
6
7
|
var uiMode: Int = UI_MODE_TYPE_NORMAL or UI_MODE_NIGHT_NO
|
|
8
|
+
internal var localeTag: String = ""
|
|
7
9
|
|
|
8
10
|
fun setTo(other: Configuration) {
|
|
9
11
|
uiMode = other.uiMode
|
|
12
|
+
localeTag = other.localeTag
|
|
10
13
|
}
|
|
11
14
|
|
|
12
15
|
companion object {
|
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
package android.content.res
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import dev.peach.app.javaFormat
|
|
4
|
+
import dev.peach.app.peachAppArrays
|
|
5
|
+
import dev.peach.app.peachAppPlurals
|
|
6
|
+
import dev.peach.app.peachAppStrings
|
|
7
|
+
|
|
8
|
+
class Resources internal constructor(val configuration: Configuration) {
|
|
9
|
+
class NotFoundException(name: String) : RuntimeException(name)
|
|
10
|
+
|
|
11
|
+
fun getString(id: Int): String =
|
|
12
|
+
resolve(peachAppStrings[id]) ?: throw NotFoundException("String resource ID #0x${id.toString(16)}")
|
|
13
|
+
|
|
14
|
+
fun getString(id: Int, vararg formatArgs: Any?): String =
|
|
15
|
+
javaFormat(configuration.localeTag, getString(id), formatArgs)
|
|
16
|
+
|
|
17
|
+
// ResourcesImpl.getQuantityText: the item the locale's plural rule
|
|
18
|
+
// selects for the quantity, then the other item
|
|
19
|
+
fun getQuantityString(id: Int, quantity: Int): String {
|
|
20
|
+
val items = resolve(peachAppPlurals[id])
|
|
21
|
+
?: throw NotFoundException("Plural resource ID #0x${id.toString(16)}")
|
|
22
|
+
val rule = pluralRule(configuration.localeTag, quantity)
|
|
23
|
+
return items[rule] ?: items["other"]
|
|
24
|
+
?: throw NotFoundException("Plural resource ID #0x${id.toString(16)} quantity=$quantity item=$rule")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fun getQuantityString(id: Int, quantity: Int, vararg formatArgs: Any?): String =
|
|
28
|
+
javaFormat(configuration.localeTag, getQuantityString(id, quantity), formatArgs)
|
|
29
|
+
|
|
30
|
+
fun getStringArray(id: Int): Array<String> =
|
|
31
|
+
resolve(peachAppArrays[id])?.toTypedArray()
|
|
32
|
+
?: throw NotFoundException("String array resource ID #0x${id.toString(16)}")
|
|
33
|
+
|
|
34
|
+
// the value the configuration's locale selects: its language and region,
|
|
35
|
+
// then its language, then the default values, as the resource table
|
|
36
|
+
// matches a locale qualifier. a plurals or array resource resolves whole
|
|
37
|
+
private fun <Value> resolve(values: Map<String, Value>?): Value? {
|
|
38
|
+
if (values == null) return null
|
|
39
|
+
val tag = configuration.localeTag
|
|
40
|
+
return values[tag] ?: values[tag.substringBefore('-')] ?: values[""]
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ICU's PluralRules.forLocale(locale).select(quantity); Intl reads the same
|
|
45
|
+
// CLDR plural rules
|
|
46
|
+
private fun pluralRule(locale: String, quantity: Int): String =
|
|
47
|
+
js("new Intl.PluralRules(locale || undefined).select(quantity)")
|
|
4
48
|
|
|
5
49
|
// the app process's resources, set once the device configuration is known at
|
|
6
50
|
// cold start; every context in the process reads the same configuration, which
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
package androidx.compose.ui.platform
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.res.Configuration
|
|
5
|
+
import androidx.compose.runtime.compositionLocalOf
|
|
6
|
+
import androidx.compose.runtime.neverEqualPolicy
|
|
7
|
+
import androidx.compose.runtime.staticCompositionLocalOf
|
|
8
|
+
|
|
9
|
+
// AndroidCompositionLocals.android.kt: the configuration and context a
|
|
10
|
+
// ComposeView's content reads, provided where its composition starts
|
|
11
|
+
val LocalConfiguration = compositionLocalOf<Configuration>(neverEqualPolicy()) {
|
|
12
|
+
noLocalProvidedFor("LocalConfiguration")
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
val LocalContext = staticCompositionLocalOf<Context> { noLocalProvidedFor("LocalContext") }
|
|
16
|
+
|
|
17
|
+
private fun noLocalProvidedFor(name: String): Nothing {
|
|
18
|
+
error("CompositionLocal $name not present")
|
|
19
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
package androidx.compose.ui.res
|
|
2
|
+
|
|
3
|
+
import android.content.res.Resources
|
|
4
|
+
import androidx.compose.runtime.Composable
|
|
5
|
+
import androidx.compose.runtime.ReadOnlyComposable
|
|
6
|
+
import androidx.compose.ui.platform.LocalConfiguration
|
|
7
|
+
import androidx.compose.ui.platform.LocalContext
|
|
8
|
+
|
|
9
|
+
// StringResources.android.kt and Resources.android.kt: a string, plural or
|
|
10
|
+
// string array resource read through the composition's context, recomposed
|
|
11
|
+
// when its configuration changes
|
|
12
|
+
|
|
13
|
+
@Composable
|
|
14
|
+
@ReadOnlyComposable
|
|
15
|
+
fun stringResource(id: Int): String {
|
|
16
|
+
val resources = resources()
|
|
17
|
+
return resources.getString(id)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@Composable
|
|
21
|
+
@ReadOnlyComposable
|
|
22
|
+
fun stringResource(id: Int, vararg formatArgs: Any): String {
|
|
23
|
+
val resources = resources()
|
|
24
|
+
return resources.getString(id, *formatArgs)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@Composable
|
|
28
|
+
@ReadOnlyComposable
|
|
29
|
+
fun pluralStringResource(id: Int, count: Int): String {
|
|
30
|
+
val resources = resources()
|
|
31
|
+
return resources.getQuantityString(id, count)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@Composable
|
|
35
|
+
@ReadOnlyComposable
|
|
36
|
+
fun pluralStringResource(id: Int, count: Int, vararg formatArgs: Any): String {
|
|
37
|
+
val resources = resources()
|
|
38
|
+
return resources.getQuantityString(id, count, *formatArgs)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@Composable
|
|
42
|
+
@ReadOnlyComposable
|
|
43
|
+
fun stringArrayResource(id: Int): Array<String> {
|
|
44
|
+
val resources = resources()
|
|
45
|
+
return resources.getStringArray(id)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Composable
|
|
49
|
+
@ReadOnlyComposable
|
|
50
|
+
internal fun resources(): Resources {
|
|
51
|
+
LocalConfiguration.current
|
|
52
|
+
return LocalContext.current.resources
|
|
53
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
package dev.peach.app
|
|
2
|
+
|
|
3
|
+
// java.util.Formatter as String.format(locale, format, args) runs it on the
|
|
4
|
+
// device, which Resources.getString(id, args) calls: the general, character,
|
|
5
|
+
// integral and decimal float conversions with their flags, width, precision
|
|
6
|
+
// and argument indexes, digits and separators in the locale's symbols. a
|
|
7
|
+
// conversion outside those (scientific, hex float, date) is rejected by name.
|
|
8
|
+
|
|
9
|
+
private val SPECIFIER = Regex("%(\\d+\\$)?([-#+ 0,(<]*)(\\d+)?(\\.\\d+)?([tT])?([a-zA-Z%])")
|
|
10
|
+
|
|
11
|
+
private class Symbols(val zero: Char, val decimal: String, val group: String)
|
|
12
|
+
|
|
13
|
+
// one part of the locale's number format, as ICU's decimal format symbols
|
|
14
|
+
// give it (Intl reads the same CLDR data)
|
|
15
|
+
private fun numberPart(locale: String, value: Double, type: String): String =
|
|
16
|
+
js("(new Intl.NumberFormat(locale || undefined, { useGrouping: true }).formatToParts(value).find((part) => part.type === type) || { value: '' }).value")
|
|
17
|
+
|
|
18
|
+
private fun symbols(locale: String) = Symbols(
|
|
19
|
+
zero = numberPart(locale, 0.0, "integer").single(),
|
|
20
|
+
decimal = numberPart(locale, 1.5, "decimal"),
|
|
21
|
+
group = numberPart(locale, 1234567.0, "group"),
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
private class Spec(val flags: String, val width: Int, val precision: Int, val text: String) {
|
|
25
|
+
fun has(flag: Char) = flag in flags
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
internal fun javaFormat(locale: String, format: String, args: Array<out Any?>): String {
|
|
29
|
+
val out = StringBuilder()
|
|
30
|
+
var ordinary = 0
|
|
31
|
+
var last = -1
|
|
32
|
+
var index = 0
|
|
33
|
+
while (index < format.length) {
|
|
34
|
+
val percent = format.indexOf('%', index)
|
|
35
|
+
if (percent == -1) {
|
|
36
|
+
out.append(format, index, format.length)
|
|
37
|
+
break
|
|
38
|
+
}
|
|
39
|
+
out.append(format, index, percent)
|
|
40
|
+
val match = SPECIFIER.matchAt(format, percent)
|
|
41
|
+
?: throw IllegalArgumentException(
|
|
42
|
+
"Conversion = '${format.getOrNull(percent + 1) ?: '%'}'",
|
|
43
|
+
)
|
|
44
|
+
index = match.range.last + 1
|
|
45
|
+
val (position, flags, width, precision, time, conversion) = match.destructured
|
|
46
|
+
if (time.isNotEmpty())
|
|
47
|
+
throw UnsupportedOperationException("peach kotlin: date conversion ${match.value} is not supported")
|
|
48
|
+
val spec = Spec(
|
|
49
|
+
flags,
|
|
50
|
+
width.toIntOrNull() ?: -1,
|
|
51
|
+
precision.drop(1).toIntOrNull() ?: -1,
|
|
52
|
+
match.value,
|
|
53
|
+
)
|
|
54
|
+
val c = conversion.single()
|
|
55
|
+
if (c == '%') {
|
|
56
|
+
out.append(justify(spec, "%"))
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
if (c == 'n') {
|
|
60
|
+
out.append('\n')
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
63
|
+
val argument = when {
|
|
64
|
+
spec.has('<') -> last
|
|
65
|
+
position.isNotEmpty() -> position.dropLast(1).toInt() - 1
|
|
66
|
+
else -> ordinary++
|
|
67
|
+
}
|
|
68
|
+
if (argument !in args.indices) throw IllegalArgumentException("Format specifier '${match.value}'")
|
|
69
|
+
last = argument
|
|
70
|
+
out.append(convert(c, args[argument], spec, locale))
|
|
71
|
+
}
|
|
72
|
+
return out.toString()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private fun justify(spec: Spec, value: String): String {
|
|
76
|
+
if (spec.width <= value.length) return value
|
|
77
|
+
val pad = " ".repeat(spec.width - value.length)
|
|
78
|
+
return if (spec.has('-')) value + pad else pad + value
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private fun typeName(value: Any?): String = when (value) {
|
|
82
|
+
is String -> "java.lang.String"
|
|
83
|
+
is Int -> "java.lang.Integer"
|
|
84
|
+
is Long -> "java.lang.Long"
|
|
85
|
+
is Short -> "java.lang.Short"
|
|
86
|
+
is Byte -> "java.lang.Byte"
|
|
87
|
+
is Double -> "java.lang.Double"
|
|
88
|
+
is Float -> "java.lang.Float"
|
|
89
|
+
is Boolean -> "java.lang.Boolean"
|
|
90
|
+
is Char -> "java.lang.Character"
|
|
91
|
+
else -> value?.let { it::class.simpleName } ?: "null"
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private fun mismatch(c: Char, value: Any?): Nothing =
|
|
95
|
+
throw IllegalArgumentException("$c != ${typeName(value)}")
|
|
96
|
+
|
|
97
|
+
private fun convert(c: Char, value: Any?, spec: Spec, locale: String): String = when (c) {
|
|
98
|
+
'b', 'B' -> {
|
|
99
|
+
val text = when (value) {
|
|
100
|
+
null -> "false"
|
|
101
|
+
is Boolean -> value.toString()
|
|
102
|
+
else -> "true"
|
|
103
|
+
}
|
|
104
|
+
justify(spec, precise(spec, if (c == 'B') text.uppercase() else text))
|
|
105
|
+
}
|
|
106
|
+
's', 'S' -> {
|
|
107
|
+
val text = precise(spec, value.toString())
|
|
108
|
+
justify(spec, if (c == 'S') text.uppercase() else text)
|
|
109
|
+
}
|
|
110
|
+
'c', 'C' -> {
|
|
111
|
+
val text = when (value) {
|
|
112
|
+
null -> "null"
|
|
113
|
+
is Char -> value.toString()
|
|
114
|
+
is Int -> codePoint(value)
|
|
115
|
+
is Byte -> codePoint(value.toInt())
|
|
116
|
+
is Short -> codePoint(value.toInt())
|
|
117
|
+
else -> mismatch(c, value)
|
|
118
|
+
}
|
|
119
|
+
justify(spec, if (c == 'C') text.uppercase() else text)
|
|
120
|
+
}
|
|
121
|
+
'd' -> when (value) {
|
|
122
|
+
null -> justify(spec, "null")
|
|
123
|
+
is Int -> decimal(spec, value.toString(), symbols(locale))
|
|
124
|
+
is Long -> decimal(spec, value.toString(), symbols(locale))
|
|
125
|
+
is Short -> decimal(spec, value.toString(), symbols(locale))
|
|
126
|
+
is Byte -> decimal(spec, value.toString(), symbols(locale))
|
|
127
|
+
else -> mismatch(c, value)
|
|
128
|
+
}
|
|
129
|
+
'x', 'X', 'o' -> {
|
|
130
|
+
val radix = if (c == 'o') 8 else 16
|
|
131
|
+
val digits = when (value) {
|
|
132
|
+
null -> null
|
|
133
|
+
is Int -> value.toUInt().toString(radix)
|
|
134
|
+
is Long -> value.toULong().toString(radix)
|
|
135
|
+
is Short -> (value.toInt() and 0xffff).toString(radix)
|
|
136
|
+
is Byte -> (value.toInt() and 0xff).toString(radix)
|
|
137
|
+
else -> mismatch(c, value)
|
|
138
|
+
}
|
|
139
|
+
if (digits == null) justify(spec, "null")
|
|
140
|
+
else {
|
|
141
|
+
val prefix = if (spec.has('#')) (if (radix == 16) "0x" else "0") else ""
|
|
142
|
+
var body = digits
|
|
143
|
+
if (spec.has('0')) body = body.padStart(spec.width - prefix.length, '0')
|
|
144
|
+
val text = prefix + body
|
|
145
|
+
justify(spec, if (c == 'X') text.uppercase() else text)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
'f' -> when (value) {
|
|
149
|
+
null -> justify(spec, "null")
|
|
150
|
+
is Double -> fixed(spec, value, symbols(locale))
|
|
151
|
+
is Float -> fixed(spec, value.toDouble(), symbols(locale))
|
|
152
|
+
else -> mismatch(c, value)
|
|
153
|
+
}
|
|
154
|
+
else -> throw UnsupportedOperationException("peach kotlin: conversion ${spec.text} is not supported")
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private fun codePoint(value: Int): String =
|
|
158
|
+
if (value < 0x10000) value.toChar().toString()
|
|
159
|
+
else {
|
|
160
|
+
val offset = value - 0x10000
|
|
161
|
+
charArrayOf((0xd800 + (offset shr 10)).toChar(), (0xdc00 + (offset and 0x3ff)).toChar()).concatToString()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private fun precise(spec: Spec, text: String) =
|
|
165
|
+
if (spec.precision >= 0 && spec.precision < text.length) text.take(spec.precision) else text
|
|
166
|
+
|
|
167
|
+
// Formatter.print(long): the sign, then the localized magnitude zero padded
|
|
168
|
+
// to the width, then the closing parenthesis, then the justification
|
|
169
|
+
private fun decimal(spec: Spec, value: String, symbols: Symbols): String {
|
|
170
|
+
val negative = value.startsWith('-')
|
|
171
|
+
return signed(spec, negative, value.removePrefix("-"), symbols)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private fun signed(spec: Spec, negative: Boolean, magnitude: String, symbols: Symbols): String {
|
|
175
|
+
val out = StringBuilder()
|
|
176
|
+
if (negative) out.append(if (spec.has('(')) '(' else '-')
|
|
177
|
+
else if (spec.has('+')) out.append('+')
|
|
178
|
+
else if (spec.has(' ')) out.append(' ')
|
|
179
|
+
val width = if (spec.width != -1 && negative && spec.has('(')) spec.width - 1 else spec.width
|
|
180
|
+
localized(out, magnitude, spec, width, symbols)
|
|
181
|
+
if (negative && spec.has('(')) out.append(')')
|
|
182
|
+
return justify(spec, out.toString())
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Formatter.localizedMagnitude: each digit in the locale's digits, grouped
|
|
186
|
+
// by three with the ',' flag, the decimal separator in place of the point,
|
|
187
|
+
// and zero padding inserted after the sign
|
|
188
|
+
private fun localized(out: StringBuilder, value: String, spec: Spec, width: Int, symbols: Symbols) {
|
|
189
|
+
val begin = out.length
|
|
190
|
+
val dot = value.indexOf('.').let { if (it == -1) value.length else it }
|
|
191
|
+
var group = spec.has(',')
|
|
192
|
+
for (index in value.indices) {
|
|
193
|
+
if (index == dot) {
|
|
194
|
+
out.append(symbols.decimal)
|
|
195
|
+
group = false
|
|
196
|
+
continue
|
|
197
|
+
}
|
|
198
|
+
out.append(symbols.zero + (value[index] - '0'))
|
|
199
|
+
if (group && index != dot - 1 && (dot - index) % 3 == 1) out.append(symbols.group)
|
|
200
|
+
}
|
|
201
|
+
if (width != -1 && spec.has('0')) while (out.length < width) out.insert(begin, symbols.zero)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Formatter.print(double) for %f: the shortest decimal digits of the value,
|
|
205
|
+
// rounded half up to the precision, as FormattedFloatingDecimal does
|
|
206
|
+
private fun fixed(spec: Spec, value: Double, symbols: Symbols): String {
|
|
207
|
+
if (value.isNaN()) return justify(spec, "NaN")
|
|
208
|
+
val negative = value < 0.0 || (value == 0.0 && 1.0 / value < 0.0)
|
|
209
|
+
if (value.isInfinite()) {
|
|
210
|
+
val text = if (negative) (if (spec.has('(')) "(Infinity)" else "-Infinity")
|
|
211
|
+
else if (spec.has('+')) "+Infinity" else if (spec.has(' ')) " Infinity" else "Infinity"
|
|
212
|
+
return justify(spec, text)
|
|
213
|
+
}
|
|
214
|
+
val precision = if (spec.precision == -1) 6 else spec.precision
|
|
215
|
+
var magnitude = roundHalfUp(shortestDigits(kotlin.math.abs(value)), precision)
|
|
216
|
+
if (precision == 0 && spec.has('#')) magnitude += "."
|
|
217
|
+
return signed(spec, negative, magnitude, symbols)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// the shortest round trip decimal of a finite non negative double as plain
|
|
221
|
+
// digits and a point, the digits Double.toString prints
|
|
222
|
+
private fun shortestDigits(value: Double): String {
|
|
223
|
+
val text = value.toString()
|
|
224
|
+
val exponentAt = text.indexOfFirst { it == 'e' || it == 'E' }
|
|
225
|
+
if (exponentAt == -1) return text
|
|
226
|
+
val mantissa = text.substring(0, exponentAt)
|
|
227
|
+
val exponent = text.substring(exponentAt + 1).toInt()
|
|
228
|
+
val point = mantissa.indexOf('.').let { if (it == -1) mantissa.length else it }
|
|
229
|
+
val digits = mantissa.replace(".", "")
|
|
230
|
+
val shifted = point + exponent
|
|
231
|
+
return when {
|
|
232
|
+
shifted <= 0 -> "0." + "0".repeat(-shifted) + digits
|
|
233
|
+
shifted >= digits.length -> digits + "0".repeat(shifted - digits.length)
|
|
234
|
+
else -> digits.substring(0, shifted) + "." + digits.substring(shifted)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private fun roundHalfUp(decimal: String, precision: Int): String {
|
|
239
|
+
val point = decimal.indexOf('.')
|
|
240
|
+
val whole = if (point == -1) decimal else decimal.substring(0, point)
|
|
241
|
+
val fraction = if (point == -1) "" else decimal.substring(point + 1)
|
|
242
|
+
if (fraction.length <= precision) {
|
|
243
|
+
val padded = fraction.padEnd(precision, '0')
|
|
244
|
+
return if (precision == 0) whole else "$whole.$padded"
|
|
245
|
+
}
|
|
246
|
+
val kept = (whole + fraction.substring(0, precision)).toCharArray()
|
|
247
|
+
if (fraction[precision] >= '5') {
|
|
248
|
+
var carry = true
|
|
249
|
+
var index = kept.size - 1
|
|
250
|
+
while (carry && index >= 0) {
|
|
251
|
+
if (kept[index] == '9') kept[index] = '0'
|
|
252
|
+
else {
|
|
253
|
+
kept[index] = kept[index] + 1
|
|
254
|
+
carry = false
|
|
255
|
+
}
|
|
256
|
+
index--
|
|
257
|
+
}
|
|
258
|
+
val digits = (if (carry) "1" else "") + kept.concatToString()
|
|
259
|
+
val wholeLength = digits.length - precision
|
|
260
|
+
return if (precision == 0) digits else digits.substring(0, wholeLength) + "." + digits.substring(wholeLength)
|
|
261
|
+
}
|
|
262
|
+
val digits = kept.concatToString()
|
|
263
|
+
return if (precision == 0) digits else digits.substring(0, whole.length) + "." + digits.substring(whole.length)
|
|
264
|
+
}
|
|
@@ -20,6 +20,8 @@ import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
|
|
|
20
20
|
import androidx.compose.runtime.Composable
|
|
21
21
|
import androidx.compose.runtime.CompositionLocalProvider
|
|
22
22
|
import androidx.compose.ui.platform.ComposeView
|
|
23
|
+
import androidx.compose.ui.platform.LocalConfiguration
|
|
24
|
+
import androidx.compose.ui.platform.LocalContext
|
|
23
25
|
import androidx.compose.ui.platform.PlatformArchitectureComponentsOwner
|
|
24
26
|
import androidx.compose.ui.platform.PlatformInsets
|
|
25
27
|
import androidx.compose.ui.platform.PlatformWindowInsets
|
|
@@ -27,6 +29,7 @@ import androidx.compose.runtime.getValue
|
|
|
27
29
|
import androidx.compose.runtime.key
|
|
28
30
|
import androidx.compose.runtime.mutableIntStateOf
|
|
29
31
|
import androidx.compose.runtime.mutableStateOf
|
|
32
|
+
import androidx.compose.runtime.neverEqualPolicy
|
|
30
33
|
import androidx.compose.runtime.setValue
|
|
31
34
|
import androidx.compose.ui.text.font.FontFamily
|
|
32
35
|
import androidx.lifecycle.Lifecycle
|
|
@@ -130,13 +133,17 @@ private class AppProcess(
|
|
|
130
133
|
var reportedNight = false
|
|
131
134
|
// the software keyboard's height above the screen's bottom edge, in pixels
|
|
132
135
|
var ime by mutableIntStateOf(0)
|
|
136
|
+
// the configuration the content view composes with: a copy the view's
|
|
137
|
+
// configuration change replaces, as AndroidComposeView's observer does
|
|
138
|
+
val configuration = mutableStateOf(Configuration(), neverEqualPolicy())
|
|
133
139
|
}
|
|
134
140
|
|
|
135
141
|
private var process: AppProcess? = null
|
|
136
142
|
|
|
137
|
-
private fun
|
|
143
|
+
private fun deviceConfiguration(night: Boolean, locale: String) = Configuration().apply {
|
|
138
144
|
uiMode = Configuration.UI_MODE_TYPE_NORMAL or
|
|
139
145
|
if (night) Configuration.UI_MODE_NIGHT_YES else Configuration.UI_MODE_NIGHT_NO
|
|
146
|
+
localeTag = locale
|
|
140
147
|
}
|
|
141
148
|
|
|
142
149
|
// a cold start in ActivityThread's order: the Application and its onCreate,
|
|
@@ -153,7 +160,7 @@ internal fun peachStartApp(
|
|
|
153
160
|
try {
|
|
154
161
|
require(backend == "graphite" || backend == "raster")
|
|
155
162
|
preparePeachDevice(locale)
|
|
156
|
-
processResources = Resources(
|
|
163
|
+
processResources = Resources(deviceConfiguration(night, locale))
|
|
157
164
|
val application = spec.createApplication()
|
|
158
165
|
application.onCreate()
|
|
159
166
|
val app = AppProcess(
|
|
@@ -235,12 +242,20 @@ private fun launchActivity(app: AppProcess, lastNonConfigurationInstance: Any?):
|
|
|
235
242
|
insets = DeviceInsets(window, app.insets) { app.ime },
|
|
236
243
|
clockRunning = { componentActivity == null || componentActivity.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) },
|
|
237
244
|
)
|
|
245
|
+
app.configuration.value = Configuration().apply { setTo(activity.resources.configuration) }
|
|
238
246
|
mount.setContent {
|
|
239
247
|
val content = @Composable { held?.let { view -> key(place) { view.Content() } }; Unit }
|
|
240
|
-
// the content view's
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
248
|
+
// the content view's context is the activity, its configuration the
|
|
249
|
+
// copy above, and its tree resolves the activity as its back
|
|
250
|
+
// dispatcher owner
|
|
251
|
+
CompositionLocalProvider(
|
|
252
|
+
LocalContext provides activity,
|
|
253
|
+
LocalConfiguration provides app.configuration.value,
|
|
254
|
+
) {
|
|
255
|
+
if (componentActivity != null)
|
|
256
|
+
CompositionLocalProvider(LocalOnBackPressedDispatcherOwner provides componentActivity, content = content)
|
|
257
|
+
else content()
|
|
258
|
+
}
|
|
244
259
|
}
|
|
245
260
|
if (app.mountId == 0) app.mountId = addMount(mount)
|
|
246
261
|
else {
|
|
@@ -319,6 +334,7 @@ private fun reportConfiguration(app: AppProcess): Boolean {
|
|
|
319
334
|
}
|
|
320
335
|
app.reportedNight = isNight(configuration)
|
|
321
336
|
app.activity.performConfigurationChanged(Configuration().apply { setTo(configuration) })
|
|
337
|
+
app.configuration.value = Configuration().apply { setTo(configuration) }
|
|
322
338
|
app.mount.night = app.reportedNight
|
|
323
339
|
return false
|
|
324
340
|
}
|
|
@@ -341,7 +357,7 @@ internal fun peachAppNight(night: Boolean) {
|
|
|
341
357
|
val app = runningApp()
|
|
342
358
|
val configuration = processResources.configuration
|
|
343
359
|
if (isNight(configuration) == night) return
|
|
344
|
-
configuration.setTo(
|
|
360
|
+
configuration.setTo(deviceConfiguration(night, app.locale))
|
|
345
361
|
app.application.onConfigurationChanged(Configuration().apply { setTo(configuration) })
|
|
346
362
|
if (!app.stopped) reportConfiguration(app)
|
|
347
363
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
package dev.contrast.kotlinapp
|
|
2
|
+
|
|
3
|
+
import androidx.compose.foundation.background
|
|
4
|
+
import androidx.compose.foundation.layout.Arrangement
|
|
5
|
+
import androidx.compose.foundation.layout.Column
|
|
6
|
+
import androidx.compose.foundation.layout.fillMaxSize
|
|
7
|
+
import androidx.compose.foundation.layout.padding
|
|
8
|
+
import androidx.compose.foundation.layout.safeDrawingPadding
|
|
9
|
+
import androidx.compose.material3.Text
|
|
10
|
+
import androidx.compose.runtime.Composable
|
|
11
|
+
import androidx.compose.ui.Modifier
|
|
12
|
+
import androidx.compose.ui.graphics.Color
|
|
13
|
+
import androidx.compose.ui.platform.LocalContext
|
|
14
|
+
import androidx.compose.ui.res.pluralStringResource
|
|
15
|
+
import androidx.compose.ui.res.stringArrayResource
|
|
16
|
+
import androidx.compose.ui.res.stringResource
|
|
17
|
+
import androidx.compose.ui.unit.dp
|
|
18
|
+
|
|
19
|
+
// string, plural and array resources read through the compose helpers and
|
|
20
|
+
// the context, each line showing what aapt2 compiled, the plural rule chose
|
|
21
|
+
// and String.format made of one rule
|
|
22
|
+
@Composable
|
|
23
|
+
fun StringsScreen() {
|
|
24
|
+
val context = LocalContext.current
|
|
25
|
+
Column(
|
|
26
|
+
Modifier.fillMaxSize().background(Color(0xFFE8EEF8)).safeDrawingPadding().padding(16.dp),
|
|
27
|
+
verticalArrangement = Arrangement.spacedBy(8.dp),
|
|
28
|
+
) {
|
|
29
|
+
Text(stringResource(R.string.title))
|
|
30
|
+
Text(stringResource(R.string.greeting, "Ada", 3))
|
|
31
|
+
Text("[" + stringResource(R.string.spaced) + "]")
|
|
32
|
+
Text("[" + stringResource(R.string.quoted) + "]")
|
|
33
|
+
Text(context.getString(R.string.escaped))
|
|
34
|
+
Text(stringResource(R.string.styled))
|
|
35
|
+
Text(stringResource(R.string.entity))
|
|
36
|
+
Text(stringResource(R.string.percent))
|
|
37
|
+
Text(stringResource(R.string.numbers, 1234567, 1.005, 42, 255, "ab"))
|
|
38
|
+
Text(context.resources.getString(R.string.numbers, -7, 2.5, -3, -1, "x"))
|
|
39
|
+
Text(stringResource(R.string.signs, 5, -5, 1234.5678, 0.125f))
|
|
40
|
+
Text(
|
|
41
|
+
pluralStringResource(R.plurals.messages, 0, 0) + " / " +
|
|
42
|
+
pluralStringResource(R.plurals.messages, 1, 1) + " / " +
|
|
43
|
+
pluralStringResource(R.plurals.messages, 2, 2),
|
|
44
|
+
)
|
|
45
|
+
Text(pluralStringResource(R.plurals.only_other, 1) + " / " + context.resources.getQuantityString(R.plurals.messages, 1, 1))
|
|
46
|
+
Text(stringArrayResource(R.array.planets).joinToString("|"))
|
|
47
|
+
Text(context.resources.getStringArray(R.array.seasons).joinToString("|"))
|
|
48
|
+
}
|
|
49
|
+
}
|