peachsim 0.1.673 → 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 +257 -236
- 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 +434 -406
- package/dist-lib/swift-compile.cjs +21 -7
- package/dist-lib/swift-compile.mjs +21 -7
- 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 +109 -88
- 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
- package/src/swift-compile.ts +31 -10
- package/src/swift-host.ts +2 -4
|
@@ -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
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// an Android module's string resources as aapt2 compiles them: each
|
|
2
|
+
// <string>, <plurals> and <string-array> under res/values and its locale
|
|
3
|
+
// qualified directories, the text with its style tags removed (getString
|
|
4
|
+
// returns the unstyled text) and aapt2's StringBuilder rules applied. a
|
|
5
|
+
// qualifier Peach does not resolve yet, or a value that is a reference, is
|
|
6
|
+
// rejected by name, never read as the default or as text.
|
|
7
|
+
import fs from 'node:fs'
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
import { decodeXmlText } from './android-xml'
|
|
10
|
+
|
|
11
|
+
// one resource's value per locale: '' for values, then a language or a
|
|
12
|
+
// language and region as a BCP 47 tag (values-en-rGB is en-GB). the device
|
|
13
|
+
// resolves a whole plurals or array resource to one locale, never per item
|
|
14
|
+
export interface AndroidResource<Value> {
|
|
15
|
+
name: string
|
|
16
|
+
values: Record<string, Value>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type PluralQuantity = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other'
|
|
20
|
+
|
|
21
|
+
export interface AndroidStringResources {
|
|
22
|
+
strings: AndroidResource<string>[]
|
|
23
|
+
plurals: AndroidResource<Partial<Record<PluralQuantity, string>>>[]
|
|
24
|
+
arrays: AndroidResource<string[]>[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const VALUES_DIR = /^values(?:-([a-z]{2,3})(?:-r([A-Z]{2}))?)?$/
|
|
28
|
+
const ELEMENT =
|
|
29
|
+
/<(string|plurals|string-array)(?=[\s/>])([^>]*?)(?:\/>|>([\s\S]*?)<\/\1\s*>)/g
|
|
30
|
+
const ITEM = /<item(?=[\s/>])([^>]*?)(?:\/>|>([\s\S]*?)<\/item\s*>)/g
|
|
31
|
+
const NAME = /\bname\s*=\s*"([^"]*)"/
|
|
32
|
+
const QUANTITY = /\bquantity\s*=\s*"([^"]*)"/
|
|
33
|
+
const QUANTITIES: readonly PluralQuantity[] = [
|
|
34
|
+
'zero',
|
|
35
|
+
'one',
|
|
36
|
+
'two',
|
|
37
|
+
'few',
|
|
38
|
+
'many',
|
|
39
|
+
'other',
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
const isQuantity = (value: string): value is PluralQuantity =>
|
|
43
|
+
QUANTITIES.some((quantity) => quantity === value)
|
|
44
|
+
|
|
45
|
+
export function readAndroidStrings(moduleDir: string): AndroidStringResources {
|
|
46
|
+
const strings = new Map<string, Record<string, string>>()
|
|
47
|
+
const plurals = new Map<
|
|
48
|
+
string,
|
|
49
|
+
Record<string, Partial<Record<PluralQuantity, string>>>
|
|
50
|
+
>()
|
|
51
|
+
const arrays = new Map<string, Record<string, string[]>>()
|
|
52
|
+
const res = path.join(moduleDir, 'src/main/res')
|
|
53
|
+
const dirs = fs.existsSync(res) ? fs.readdirSync(res).sort() : []
|
|
54
|
+
for (const dir of dirs) {
|
|
55
|
+
if (!dir.startsWith('values')) continue
|
|
56
|
+
const qualifier = VALUES_DIR.exec(dir)
|
|
57
|
+
for (const entry of fs.readdirSync(path.join(res, dir)).sort()) {
|
|
58
|
+
if (!entry.endsWith('.xml')) continue
|
|
59
|
+
const file = path.join(res, dir, entry)
|
|
60
|
+
const text = fs.readFileSync(file, 'utf8').replace(/<!--[\s\S]*?-->/g, '')
|
|
61
|
+
for (const match of text.matchAll(ELEMENT)) {
|
|
62
|
+
const kind = match[1] ?? ''
|
|
63
|
+
const name = NAME.exec(match[2] ?? '')?.[1]
|
|
64
|
+
if (!name) throw new Error(`${file}: a <${kind}> without a name`)
|
|
65
|
+
if (!qualifier)
|
|
66
|
+
throw new Error(
|
|
67
|
+
`${file}: ${kind} ${name} under ${dir} is not resolved by peach kotlin yet (only values, values-<language> and values-<language>-r<REGION>)`,
|
|
68
|
+
)
|
|
69
|
+
const locale = qualifier[2]
|
|
70
|
+
? `${qualifier[1]}-${qualifier[2]}`
|
|
71
|
+
: (qualifier[1] ?? '')
|
|
72
|
+
const body = match[3] ?? ''
|
|
73
|
+
const where = `${file}: ${kind} ${name}`
|
|
74
|
+
if (kind === 'string')
|
|
75
|
+
add(strings, name, locale, compileString(body, where), where)
|
|
76
|
+
else if (kind === 'plurals') {
|
|
77
|
+
const items: Partial<Record<PluralQuantity, string>> = {}
|
|
78
|
+
for (const item of body.matchAll(ITEM)) {
|
|
79
|
+
const quantity = QUANTITY.exec(item[1] ?? '')?.[1] ?? ''
|
|
80
|
+
if (!isQuantity(quantity))
|
|
81
|
+
throw new Error(`${where} has an item with quantity "${quantity}"`)
|
|
82
|
+
if (quantity in items) throw new Error(`${where} defines ${quantity} twice`)
|
|
83
|
+
items[quantity] = compileString(item[2] ?? '', `${where} ${quantity}`)
|
|
84
|
+
}
|
|
85
|
+
add(plurals, name, locale, items, where)
|
|
86
|
+
} else
|
|
87
|
+
add(
|
|
88
|
+
arrays,
|
|
89
|
+
name,
|
|
90
|
+
locale,
|
|
91
|
+
[...body.matchAll(ITEM)].map((item, index) =>
|
|
92
|
+
compileString(item[2] ?? '', `${where} item ${index}`),
|
|
93
|
+
),
|
|
94
|
+
where,
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { strings: sorted(strings), plurals: sorted(plurals), arrays: sorted(arrays) }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function add<Value>(
|
|
103
|
+
byName: Map<string, Record<string, Value>>,
|
|
104
|
+
name: string,
|
|
105
|
+
locale: string,
|
|
106
|
+
value: Value,
|
|
107
|
+
where: string,
|
|
108
|
+
) {
|
|
109
|
+
const values = byName.get(name) ?? {}
|
|
110
|
+
if (locale in values) throw new Error(`${where} is defined twice for one locale`)
|
|
111
|
+
values[locale] = value
|
|
112
|
+
byName.set(name, values)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// the R class code compiles against lists only the resources the default
|
|
116
|
+
// values define (AGP's ParseLibraryResourcesTask reads the unqualified
|
|
117
|
+
// directories), so a resource only a qualified directory defines has no field
|
|
118
|
+
const sorted = <Value>(
|
|
119
|
+
byName: Map<string, Record<string, Value>>,
|
|
120
|
+
): AndroidResource<Value>[] =>
|
|
121
|
+
[...byName.entries()]
|
|
122
|
+
.filter(([, values]) => '' in values)
|
|
123
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
124
|
+
.map(([name, values]) => ({ name, values }))
|
|
125
|
+
|
|
126
|
+
// aapt2's StringBuilder over the decoded text: outside double quotes a run of
|
|
127
|
+
// whitespace is one space and the ends are trimmed, an unescaped apostrophe is
|
|
128
|
+
// an error; a double quote toggles quoting and is dropped; a backslash escapes
|
|
129
|
+
// the next character (\n, \t and \uXXXX, any other character as itself). a
|
|
130
|
+
// value beginning with @ or ? is a reference aapt2 resolves, which Peach does
|
|
131
|
+
// not yet
|
|
132
|
+
function compileString(raw: string, where: string): string {
|
|
133
|
+
const text = decodeXmlText(raw.replace(/<[^>]*>/g, ''), where)
|
|
134
|
+
if (/^\s*[@?]/.test(text))
|
|
135
|
+
throw new Error(`${where} is a resource reference, not resolved by peach kotlin yet`)
|
|
136
|
+
const out: { char: string; soft: boolean }[] = []
|
|
137
|
+
let quoted = false
|
|
138
|
+
for (let index = 0; index < text.length; index++) {
|
|
139
|
+
const char = text[index] ?? ''
|
|
140
|
+
if (char === '\\') {
|
|
141
|
+
const next = text[index + 1]
|
|
142
|
+
if (next === undefined) break
|
|
143
|
+
index++
|
|
144
|
+
if (next === 'n') out.push({ char: '\n', soft: false })
|
|
145
|
+
else if (next === 't') out.push({ char: '\t', soft: false })
|
|
146
|
+
else if (next === 'u') {
|
|
147
|
+
const hex = text.slice(index + 1, index + 5)
|
|
148
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex))
|
|
149
|
+
throw new Error(`${where} has an invalid unicode escape \\u${hex}`)
|
|
150
|
+
out.push({ char: String.fromCharCode(Number.parseInt(hex, 16)), soft: false })
|
|
151
|
+
index += 4
|
|
152
|
+
} else out.push({ char: next, soft: false })
|
|
153
|
+
} else if (char === '"') quoted = !quoted
|
|
154
|
+
else if (char === "'" && !quoted)
|
|
155
|
+
throw new Error(`${where} has an unescaped apostrophe`)
|
|
156
|
+
else if (!quoted && /\s/.test(char)) {
|
|
157
|
+
if (out.at(-1)?.soft !== true) out.push({ char: ' ', soft: true })
|
|
158
|
+
} else out.push({ char, soft: false })
|
|
159
|
+
}
|
|
160
|
+
while (out[0]?.soft) out.shift()
|
|
161
|
+
while (out.at(-1)?.soft) out.pop()
|
|
162
|
+
return out.map((part) => part.char).join('')
|
|
163
|
+
}
|
package/src/android-xml.ts
CHANGED
|
@@ -21,7 +21,7 @@ const ENTITIES: Record<string, string> = {
|
|
|
21
21
|
const TAG = /<(\/?)([\w:.-]+)((?:\s+[\w:.-]+\s*=\s*(?:"[^"]*"|'[^']*'))*)\s*(\/?)>/y
|
|
22
22
|
const ATTRIBUTE = /([\w:.-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g
|
|
23
23
|
|
|
24
|
-
function
|
|
24
|
+
export function decodeXmlText(value: string, file: string): string {
|
|
25
25
|
return value.replace(/&(#x[0-9a-fA-F]+|#\d+|\w+);/g, (_match, entity: string) => {
|
|
26
26
|
if (entity.startsWith('#x'))
|
|
27
27
|
return String.fromCodePoint(Number.parseInt(entity.slice(2), 16))
|
|
@@ -42,7 +42,7 @@ export function parseXml(text: string, file: string): XmlElement {
|
|
|
42
42
|
const open = body.indexOf('<', index)
|
|
43
43
|
const content = body.slice(index, open === -1 ? body.length : open)
|
|
44
44
|
const parent = stack.at(-1)
|
|
45
|
-
if (parent && content.trim()) parent.text +=
|
|
45
|
+
if (parent && content.trim()) parent.text += decodeXmlText(content, file)
|
|
46
46
|
if (open === -1) break
|
|
47
47
|
TAG.lastIndex = open
|
|
48
48
|
const match = TAG.exec(body)
|
|
@@ -59,7 +59,7 @@ export function parseXml(text: string, file: string): XmlElement {
|
|
|
59
59
|
}
|
|
60
60
|
const attributes: Record<string, string> = {}
|
|
61
61
|
for (const [, key, double, single] of (rawAttributes ?? '').matchAll(ATTRIBUTE))
|
|
62
|
-
if (key) attributes[key] =
|
|
62
|
+
if (key) attributes[key] = decodeXmlText(double ?? single ?? '', file)
|
|
63
63
|
const element: XmlElement = { name, attributes, children: [], text: '' }
|
|
64
64
|
stack.at(-1)?.children.push(element)
|
|
65
65
|
if (!selfClosing) stack.push(element)
|
package/src/kotlin-app.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import fs from 'node:fs'
|
|
7
7
|
import path from 'node:path'
|
|
8
|
+
import { readAndroidStrings, type AndroidStringResources } from './android-strings'
|
|
8
9
|
import {
|
|
9
10
|
frameworkColor,
|
|
10
11
|
readResourceValues,
|
|
@@ -47,6 +48,8 @@ export interface KotlinAppProject {
|
|
|
47
48
|
// how the activity's window meets the keyboard: pan over a covered focused
|
|
48
49
|
// view, resize above the keyboard, or nothing
|
|
49
50
|
softInputAdjust: 'pan' | 'resize' | 'nothing'
|
|
51
|
+
// the module's string resources, which its R class names
|
|
52
|
+
strings: AndroidStringResources
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
export interface AndroidPins {
|
|
@@ -501,5 +504,6 @@ export function readKotlinApp(
|
|
|
501
504
|
predictiveBack,
|
|
502
505
|
handlesUiMode,
|
|
503
506
|
softInputAdjust,
|
|
507
|
+
strings: readAndroidStrings(moduleDir),
|
|
504
508
|
}
|
|
505
509
|
}
|