pythonnative 0.35.0__py3-none-any.whl → 0.37.0__py3-none-any.whl
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.
- pythonnative/__init__.py +6 -4
- pythonnative/_ios_log.py +1 -1
- pythonnative/alerts.py +34 -88
- pythonnative/bootstrap.py +90 -0
- pythonnative/bridge/__init__.py +306 -0
- pythonnative/bridge/android.py +78 -0
- pythonnative/bridge/codec.py +164 -0
- pythonnative/bridge/fake.py +236 -0
- pythonnative/bridge/ios.py +138 -0
- pythonnative/cli/pn.py +24 -6
- pythonnative/components/layout.py +65 -3
- pythonnative/gestures.py +13 -10
- pythonnative/hooks.py +41 -26
- pythonnative/hosts/__init__.py +13 -36
- pythonnative/hosts/native.py +348 -0
- pythonnative/images.py +6 -5
- pythonnative/mutations.py +21 -0
- pythonnative/native_modules/__init__.py +30 -29
- pythonnative/native_modules/app_state.py +10 -6
- pythonnative/native_modules/battery.py +20 -84
- pythonnative/native_modules/biometrics.py +11 -124
- pythonnative/native_modules/camera.py +15 -291
- pythonnative/native_modules/clipboard.py +14 -109
- pythonnative/native_modules/desktop.py +322 -0
- pythonnative/native_modules/file_system.py +15 -21
- pythonnative/native_modules/haptics.py +19 -129
- pythonnative/native_modules/linking.py +24 -137
- pythonnative/native_modules/location.py +28 -180
- pythonnative/native_modules/net_info.py +25 -135
- pythonnative/native_modules/notifications.py +36 -257
- pythonnative/native_modules/permissions.py +20 -184
- pythonnative/native_modules/registry.py +513 -0
- pythonnative/native_modules/secure_store.py +18 -155
- pythonnative/native_modules/share.py +8 -113
- pythonnative/native_views/__init__.py +67 -86
- pythonnative/native_views/bridge_backend.py +264 -0
- pythonnative/platform.py +19 -15
- pythonnative/platform_metrics.py +3 -3
- pythonnative/project/android.py +0 -3
- pythonnative/project/builder.py +38 -16
- pythonnative/project/config.py +12 -2
- pythonnative/project/plugins.py +366 -0
- pythonnative/py.typed +0 -0
- pythonnative/runtime.py +35 -203
- pythonnative/sdk/__init__.py +48 -33
- pythonnative/sdk/_components.py +57 -76
- pythonnative/storage.py +18 -243
- pythonnative/templates/android_template/app/build.gradle +6 -9
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +34 -166
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +13 -151
- pythonnative/templates/android_template/pythonnative/build.gradle +48 -0
- pythonnative/templates/android_template/pythonnative/src/main/AndroidManifest.xml +11 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/PNBridge.kt +214 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/PythonHost.kt +20 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/animation/AnimationSpecs.kt +141 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/animation/PNAnimator.kt +249 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/JsonUtil.kt +175 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/MainThread.kt +32 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNLog.kt +33 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNRegistry.kt +82 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNTransaction.kt +80 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/TransactionApplier.kt +115 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/ViewRegistry.kt +74 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/BuiltinComponents.kt +35 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ButtonManager.kt +33 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ComponentManager.kt +213 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ContainerManagers.kt +69 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ControlManagers.kt +186 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/DatePickerManager.kt +123 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ImageLoader.kt +141 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ImageManager.kt +156 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ModalManager.kt +134 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PNColor.kt +201 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PickerManager.kt +87 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PortalManager.kt +55 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PressableManager.kt +122 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ScrollViewManager.kt +214 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/SegmentedControlManager.kt +113 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/StatusBarManager.kt +52 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TabBarManager.kt +131 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TextInputManager.kt +245 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TextManager.kt +233 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ViewStyler.kt +355 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/VirtualListManager.kt +179 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/WebViewManager.kt +149 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureArbiter.kt +233 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureCoordinator.kt +127 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureRecognizers.kt +527 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/BuiltinModules.kt +73 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/DeviceModules.kt +316 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/HostModule.kt +97 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/MediaModules.kt +200 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/NativeModule.kt +135 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/NotificationsModule.kt +100 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/PermissionsModule.kt +88 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/StorageModules.kt +89 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/SystemModules.kt +178 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/plugins/GeneratedPlugins.kt +16 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/Navigator.kt +116 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/PNScreenFragment.kt +223 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/ScreenRegistry.kt +80 -0
- pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNAccessibilityDelegate.kt +5 -4
- pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNBorderDrawable.kt +12 -3
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/views/PNEditText.kt +18 -0
- pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNFrameLayout.kt +9 -10
- pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNVirtualListView.java +9 -12
- pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/AnimationSpecsTest.kt +75 -0
- pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/GestureArbiterTest.kt +197 -0
- pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PNColorTest.kt +47 -0
- pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PNTransactionTest.kt +78 -0
- pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PromiseTest.kt +75 -0
- pythonnative/templates/android_template/settings.gradle +1 -0
- pythonnative/templates/ios_template/PythonNativeKit/Package.swift +21 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Animation/PNAnimator.swift +301 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNBridge.swift +191 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNRegistry.swift +140 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNTransaction.swift +165 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNViewRegistry.swift +53 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNButtonManagers.swift +206 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNColor.swift +173 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNComponentManager.swift +183 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNContainerView.swift +56 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNControlManagers.swift +281 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNImageManager.swift +221 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNOverlayManagers.swift +243 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNPressableManager.swift +114 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNScrollViewManager.swift +197 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTabBarManager.swift +106 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTextInputManager.swift +289 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTextManager.swift +230 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTransform.swift +82 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewManager.swift +86 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewState.swift +87 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewStyler.swift +340 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNVirtualListManager.swift +257 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNWebViewManager.swift +124 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Gestures/PNGestureCoordinator.swift +260 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/AlertModule.swift +92 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/HostModule.swift +158 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/LifecycleModules.swift +174 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/MediaModules.swift +177 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PNNativeModule.swift +125 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PermissionModules.swift +251 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/SystemModules.swift +251 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Plugins/PNPluginRegistration.swift +8 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Screens/PNScreenRegistry.swift +46 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Screens/PNViewController.swift +251 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNCompat.swift +46 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNJSON.swift +173 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNLog.swift +47 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNWindow.swift +51 -0
- pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNColorTests.swift +50 -0
- pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNGestureTests.swift +101 -0
- pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNManagerTests.swift +124 -0
- pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNModuleTests.swift +98 -0
- pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNTransactionTests.swift +91 -0
- pythonnative/templates/ios_template/ios_template/AppDelegate.swift +8 -50
- pythonnative/templates/ios_template/ios_template/Info.plist +4 -4
- pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +38 -46
- pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +11 -16
- pythonnative/templates/ios_template/ios_template/ViewController.swift +12 -142
- pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +47 -1
- pythonnative/utils.py +4 -82
- pythonnative/virtual_rows.py +3 -3
- {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/METADATA +3 -5
- pythonnative-0.37.0.dist-info/RECORD +263 -0
- pythonnative/hosts/android.py +0 -269
- pythonnative/hosts/ios.py +0 -404
- pythonnative/native_views/android.py +0 -3702
- pythonnative/native_views/ios.py +0 -5253
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt +0 -43
- pythonnative-0.35.0.dist-info/RECORD +0 -155
- {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
package com.pythonnative.runtime.components
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.Bitmap
|
|
5
|
+
import android.graphics.BitmapFactory
|
|
6
|
+
import android.util.LruCache
|
|
7
|
+
import com.pythonnative.runtime.bridge.MainThread
|
|
8
|
+
import java.io.File
|
|
9
|
+
import java.io.FileOutputStream
|
|
10
|
+
import java.net.HttpURLConnection
|
|
11
|
+
import java.net.URL
|
|
12
|
+
import java.security.MessageDigest
|
|
13
|
+
import java.util.concurrent.Executors
|
|
14
|
+
import kotlin.math.max
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Image fetching and decoding for the `Image` manager.
|
|
18
|
+
*
|
|
19
|
+
* Remote images are downloaded on a small background pool into a disk
|
|
20
|
+
* cache under `cacheDir/pn_images` (keyed by the SHA-256 of the URL),
|
|
21
|
+
* decoded with `inSampleSize` downsampling to the target size, and kept
|
|
22
|
+
* in a memory `LruCache`. Callbacks run on the main thread.
|
|
23
|
+
*/
|
|
24
|
+
object ImageLoader {
|
|
25
|
+
private val executor = Executors.newFixedThreadPool(3) { runnable ->
|
|
26
|
+
Thread(runnable, "pn-image").apply { isDaemon = true }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private val memory: LruCache<String, Bitmap> by lazy {
|
|
30
|
+
val maxKb = (Runtime.getRuntime().maxMemory() / 1024L).toInt()
|
|
31
|
+
object : LruCache<String, Bitmap>(max(4 * 1024, maxKb / 8)) {
|
|
32
|
+
override fun sizeOf(key: String, value: Bitmap): Int = max(1, value.byteCount / 1024)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Result callback: exactly one of `bitmap` or `error` is non-null. */
|
|
37
|
+
fun interface Callback {
|
|
38
|
+
fun onResult(bitmap: Bitmap?, error: String?)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Load `url` (http/https) into a bitmap sized for `targetW` x `targetH` pixels. */
|
|
42
|
+
fun loadRemote(context: Context, url: String, targetW: Int, targetH: Int, callback: Callback) {
|
|
43
|
+
val key = "$url@$targetW"
|
|
44
|
+
memory.get(key)?.let {
|
|
45
|
+
callback.onResult(it, null)
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
val cacheDir = File(context.cacheDir, "pn_images")
|
|
49
|
+
executor.execute {
|
|
50
|
+
try {
|
|
51
|
+
val file = cachedFile(cacheDir, url)
|
|
52
|
+
if (!file.exists() || file.length() == 0L) download(url, file)
|
|
53
|
+
val bitmap = decodeDownsampled(file.absolutePath, targetW, targetH)
|
|
54
|
+
if (bitmap == null) {
|
|
55
|
+
file.delete()
|
|
56
|
+
deliver(callback, null, "decode failed")
|
|
57
|
+
} else {
|
|
58
|
+
memory.put(key, bitmap)
|
|
59
|
+
deliver(callback, bitmap, null)
|
|
60
|
+
}
|
|
61
|
+
} catch (e: Exception) {
|
|
62
|
+
deliver(callback, null, e.message ?: e.javaClass.simpleName)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Decode a local file on a background thread. */
|
|
68
|
+
fun loadFile(path: String, targetW: Int, targetH: Int, callback: Callback) {
|
|
69
|
+
executor.execute {
|
|
70
|
+
val bitmap = try {
|
|
71
|
+
decodeDownsampled(path, targetW, targetH)
|
|
72
|
+
} catch (e: Exception) {
|
|
73
|
+
null
|
|
74
|
+
}
|
|
75
|
+
deliver(callback, bitmap, if (bitmap == null) "decode failed" else null)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Decode `path` with `inSampleSize` chosen so the result still covers the target. */
|
|
80
|
+
fun decodeDownsampled(path: String, targetW: Int, targetH: Int): Bitmap? {
|
|
81
|
+
val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
|
82
|
+
BitmapFactory.decodeFile(path, bounds)
|
|
83
|
+
val srcW = bounds.outWidth
|
|
84
|
+
val srcH = bounds.outHeight
|
|
85
|
+
if (srcW <= 0 || srcH <= 0) return null
|
|
86
|
+
var tw = if (targetW > 0) targetW else srcW
|
|
87
|
+
var th = if (targetH > 0) targetH else tw
|
|
88
|
+
if (tw <= 0) tw = srcW
|
|
89
|
+
if (th <= 0) th = srcH
|
|
90
|
+
var sample = 1
|
|
91
|
+
while (srcW / (sample * 2) >= tw && srcH / (sample * 2) >= th) sample *= 2
|
|
92
|
+
val opts = BitmapFactory.Options().apply { inSampleSize = sample }
|
|
93
|
+
return BitmapFactory.decodeFile(path, opts)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private fun cachedFile(dir: File, url: String): File {
|
|
97
|
+
if (!dir.exists()) dir.mkdirs()
|
|
98
|
+
val digest = MessageDigest.getInstance("SHA-256").digest(url.toByteArray(Charsets.UTF_8))
|
|
99
|
+
val hex = digest.joinToString("") { String.format("%02x", it) }
|
|
100
|
+
val path = url.substringBefore('?').substringBefore('#')
|
|
101
|
+
val ext = path.substringAfterLast('.', "").takeIf { it.length in 1..5 && it.all { c -> c.isLetterOrDigit() } }
|
|
102
|
+
return File(dir, if (ext != null) "$hex.$ext" else hex)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private fun download(url: String, target: File) {
|
|
106
|
+
var current = url
|
|
107
|
+
var redirects = 0
|
|
108
|
+
while (true) {
|
|
109
|
+
val connection = URL(current).openConnection() as HttpURLConnection
|
|
110
|
+
connection.connectTimeout = 15_000
|
|
111
|
+
connection.readTimeout = 30_000
|
|
112
|
+
connection.instanceFollowRedirects = true
|
|
113
|
+
connection.setRequestProperty("User-Agent", "PythonNative/Android")
|
|
114
|
+
try {
|
|
115
|
+
val code = connection.responseCode
|
|
116
|
+
if (code in 300..399 && redirects < 5) {
|
|
117
|
+
val location = connection.getHeaderField("Location") ?: throw IllegalStateException("redirect without Location")
|
|
118
|
+
current = URL(URL(current), location).toString()
|
|
119
|
+
redirects++
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
if (code !in 200..299) throw IllegalStateException("HTTP $code")
|
|
123
|
+
val tmp = File(target.parentFile, target.name + ".part")
|
|
124
|
+
connection.inputStream.use { input ->
|
|
125
|
+
FileOutputStream(tmp).use { output -> input.copyTo(output) }
|
|
126
|
+
}
|
|
127
|
+
if (!tmp.renameTo(target)) {
|
|
128
|
+
tmp.copyTo(target, overwrite = true)
|
|
129
|
+
tmp.delete()
|
|
130
|
+
}
|
|
131
|
+
return
|
|
132
|
+
} finally {
|
|
133
|
+
connection.disconnect()
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private fun deliver(callback: Callback, bitmap: Bitmap?, error: String?) {
|
|
139
|
+
MainThread.post { callback.onResult(bitmap, error) }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
package com.pythonnative.runtime.components
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.res.ColorStateList
|
|
5
|
+
import android.graphics.BitmapFactory
|
|
6
|
+
import android.util.Base64
|
|
7
|
+
import android.view.View
|
|
8
|
+
import android.widget.ImageView
|
|
9
|
+
import com.pythonnative.runtime.bridge.PNLog
|
|
10
|
+
import com.pythonnative.runtime.bridge.str
|
|
11
|
+
import com.pythonnative.runtime.bridge.value
|
|
12
|
+
import org.json.JSONObject
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* `Image` element. Sources: `http(s)` URLs (downloaded and cached by
|
|
16
|
+
* [ImageLoader]), absolute file paths, drawable resource names, and
|
|
17
|
+
* base64 `data:` URIs. Fires `on_load` / `on_error`.
|
|
18
|
+
*/
|
|
19
|
+
class ImageManager : ComponentManager() {
|
|
20
|
+
override fun createView(context: Context, tag: Long, props: JSONObject): View = ImageView(context)
|
|
21
|
+
|
|
22
|
+
override fun applyProps(view: View, props: JSONObject, initial: Boolean) {
|
|
23
|
+
val iv = view as ImageView
|
|
24
|
+
PNColor.parse(props.value("tint_color"))?.let { iv.imageTintList = ColorStateList.valueOf(it) }
|
|
25
|
+
if (props.has("tint_color") && props.value("tint_color") == null) iv.imageTintList = null
|
|
26
|
+
PNColor.parse(props.value("placeholder_color"))?.let { iv.setBackgroundColor(it) }
|
|
27
|
+
if (props.has("source")) {
|
|
28
|
+
val source = sourceString(props.value("source"))
|
|
29
|
+
if (source != null) loadSource(iv, source) else iv.setImageDrawable(null)
|
|
30
|
+
}
|
|
31
|
+
val mode = props.str("resize_mode") ?: props.str("scale_type")
|
|
32
|
+
if (mode != null) {
|
|
33
|
+
iv.scaleType = when (mode) {
|
|
34
|
+
"cover" -> ImageView.ScaleType.CENTER_CROP
|
|
35
|
+
"contain" -> ImageView.ScaleType.FIT_CENTER
|
|
36
|
+
"stretch" -> ImageView.ScaleType.FIT_XY
|
|
37
|
+
"center", "repeat" -> ImageView.ScaleType.CENTER
|
|
38
|
+
else -> iv.scaleType
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
ViewStyler.apply(iv, props)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private fun sourceString(value: Any?): String? {
|
|
45
|
+
return when (value) {
|
|
46
|
+
null -> null
|
|
47
|
+
is JSONObject -> value.str("uri") ?: value.str("url") ?: value.str("path")
|
|
48
|
+
else -> value.toString().takeIf { it.isNotEmpty() }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private fun loadSource(iv: ImageView, source: String) {
|
|
53
|
+
val state = stateOf(iv)
|
|
54
|
+
state["pending_uri"] = source
|
|
55
|
+
try {
|
|
56
|
+
when {
|
|
57
|
+
source.startsWith("data:") -> loadDataUri(iv, source)
|
|
58
|
+
source.startsWith("http://") || source.startsWith("https://") -> {
|
|
59
|
+
val (tw, th) = targetSize(iv)
|
|
60
|
+
ImageLoader.loadRemote(iv.context, source, tw, th) { bitmap, error ->
|
|
61
|
+
if (stateOf(iv)["pending_uri"] != source) return@loadRemote
|
|
62
|
+
if (bitmap != null) {
|
|
63
|
+
iv.setImageBitmap(bitmap)
|
|
64
|
+
fire(iv, "on_load")
|
|
65
|
+
} else {
|
|
66
|
+
fire(iv, "on_error", error ?: "load failed")
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
source.startsWith("/") || source.startsWith("file://") -> {
|
|
71
|
+
val path = source.removePrefix("file://")
|
|
72
|
+
val (tw, th) = targetSize(iv)
|
|
73
|
+
ImageLoader.loadFile(path, tw, th) { bitmap, error ->
|
|
74
|
+
if (stateOf(iv)["pending_uri"] != source) return@loadFile
|
|
75
|
+
if (bitmap != null) {
|
|
76
|
+
iv.setImageBitmap(bitmap)
|
|
77
|
+
fire(iv, "on_load")
|
|
78
|
+
} else {
|
|
79
|
+
fire(iv, "on_error", error ?: "decode failed")
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else -> {
|
|
84
|
+
val ctx = iv.context
|
|
85
|
+
val name = source.substringBeforeLast('.', source)
|
|
86
|
+
val resId = ctx.resources.getIdentifier(name, "drawable", ctx.packageName)
|
|
87
|
+
.takeIf { it != 0 }
|
|
88
|
+
?: ctx.resources.getIdentifier(name, "mipmap", ctx.packageName)
|
|
89
|
+
if (resId != 0) {
|
|
90
|
+
iv.setImageResource(resId)
|
|
91
|
+
fire(iv, "on_load")
|
|
92
|
+
} else {
|
|
93
|
+
fire(iv, "on_error", "drawable '$name' not found")
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} catch (e: Exception) {
|
|
98
|
+
PNLog.swallowed("ImageManager.loadSource", e)
|
|
99
|
+
fire(iv, "on_error", e.message ?: "load failed")
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private fun loadDataUri(iv: ImageView, source: String) {
|
|
104
|
+
try {
|
|
105
|
+
val payload = source.substringAfter(',', "")
|
|
106
|
+
val raw = Base64.decode(payload, Base64.DEFAULT)
|
|
107
|
+
val bitmap = BitmapFactory.decodeByteArray(raw, 0, raw.size)
|
|
108
|
+
if (bitmap != null) {
|
|
109
|
+
iv.setImageBitmap(bitmap)
|
|
110
|
+
fire(iv, "on_load")
|
|
111
|
+
} else {
|
|
112
|
+
fire(iv, "on_error", "data URI decode failed")
|
|
113
|
+
}
|
|
114
|
+
} catch (e: Exception) {
|
|
115
|
+
fire(iv, "on_error", "data URI decode failed")
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private fun targetSize(iv: ImageView): Pair<Int, Int> {
|
|
120
|
+
var w = iv.width
|
|
121
|
+
var h = iv.height
|
|
122
|
+
val frame = recordOf(iv)?.frame
|
|
123
|
+
if (w <= 0 && frame != null) {
|
|
124
|
+
w = px(frame[2])
|
|
125
|
+
h = px(frame[3])
|
|
126
|
+
}
|
|
127
|
+
if (w <= 0) {
|
|
128
|
+
val metrics = iv.context.resources.displayMetrics
|
|
129
|
+
w = metrics.widthPixels
|
|
130
|
+
h = metrics.heightPixels
|
|
131
|
+
}
|
|
132
|
+
if (h <= 0) h = w
|
|
133
|
+
return Pair(w, h)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
override fun measure(view: View, maxWidth: Double, maxHeight: Double): FloatArray {
|
|
137
|
+
val iv = view as ImageView
|
|
138
|
+
val drawable = iv.drawable ?: return floatArrayOf(0f, 0f)
|
|
139
|
+
val density = view.context.resources.displayMetrics.density
|
|
140
|
+
// Bitmaps decode at device density, so their pixel size is already a dp-ish measure.
|
|
141
|
+
var w = drawable.intrinsicWidth / density
|
|
142
|
+
var h = drawable.intrinsicHeight / density
|
|
143
|
+
if (w <= 0f || h <= 0f) return floatArrayOf(0f, 0f)
|
|
144
|
+
val maxW = if (maxWidth.isFinite() && maxWidth < 1e6) maxWidth.toFloat() else Float.MAX_VALUE
|
|
145
|
+
val maxH = if (maxHeight.isFinite() && maxHeight < 1e6) maxHeight.toFloat() else Float.MAX_VALUE
|
|
146
|
+
if (w > maxW) {
|
|
147
|
+
h *= maxW / w
|
|
148
|
+
w = maxW
|
|
149
|
+
}
|
|
150
|
+
if (h > maxH) {
|
|
151
|
+
w *= maxH / h
|
|
152
|
+
h = maxH
|
|
153
|
+
}
|
|
154
|
+
return floatArrayOf(w, h)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
package com.pythonnative.runtime.components
|
|
2
|
+
|
|
3
|
+
import android.app.Dialog
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.graphics.drawable.ColorDrawable
|
|
6
|
+
import android.view.View
|
|
7
|
+
import android.view.ViewGroup
|
|
8
|
+
import android.view.WindowManager
|
|
9
|
+
import android.widget.FrameLayout
|
|
10
|
+
import com.pythonnative.runtime.bridge.JsonUtil
|
|
11
|
+
import com.pythonnative.runtime.bridge.str
|
|
12
|
+
import com.pythonnative.runtime.bridge.value
|
|
13
|
+
import org.json.JSONObject
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* `Modal` element: real modal presentation backed by a `Dialog`.
|
|
17
|
+
*
|
|
18
|
+
* The on-tree placeholder is a hidden `View`. When `visible` flips to
|
|
19
|
+
* `true`, a `Dialog` hosting a `FrameLayout` is shown and the
|
|
20
|
+
* reconciler's child inserts are forwarded into that content view.
|
|
21
|
+
*/
|
|
22
|
+
class ModalManager : ComponentManager() {
|
|
23
|
+
override fun createView(context: Context, tag: Long, props: JSONObject): View {
|
|
24
|
+
val placeholder = View(context)
|
|
25
|
+
placeholder.visibility = View.GONE
|
|
26
|
+
return placeholder
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
override fun applyProps(view: View, props: JSONObject, initial: Boolean) {
|
|
30
|
+
val state = stateOf(view)
|
|
31
|
+
// Only react when `visible` itself changed; a re-render while the
|
|
32
|
+
// dialog is open must not tear it down.
|
|
33
|
+
if (props.has("visible")) {
|
|
34
|
+
val visible = JsonUtil.truthy(props.value("visible"))
|
|
35
|
+
if (visible && state["dialog"] == null) present(view)
|
|
36
|
+
else if (!visible && state["dialog"] != null) dismiss(view, fromUser = false)
|
|
37
|
+
}
|
|
38
|
+
val dialog = state["dialog"] as? Dialog
|
|
39
|
+
if (dialog != null && props.has("dismiss_on_backdrop")) {
|
|
40
|
+
dialog.setCanceledOnTouchOutside(props.value("dismiss_on_backdrop") != false)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
override fun insertChild(parent: View, child: View, index: Int) {
|
|
45
|
+
val state = stateOf(parent)
|
|
46
|
+
val content = state["content_view"] as? FrameLayout
|
|
47
|
+
if (content != null) {
|
|
48
|
+
ViewChildren.insert(content, child, index)
|
|
49
|
+
} else {
|
|
50
|
+
@Suppress("UNCHECKED_CAST")
|
|
51
|
+
val pending = state.getOrPut("pending") { ArrayList<View>() } as ArrayList<View>
|
|
52
|
+
pending.remove(child)
|
|
53
|
+
pending.add(index.coerceIn(0, pending.size), child)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
override fun removeChild(parent: View, child: View) {
|
|
58
|
+
val state = stateOf(parent)
|
|
59
|
+
val content = state["content_view"] as? FrameLayout
|
|
60
|
+
if (content != null) {
|
|
61
|
+
content.removeView(child)
|
|
62
|
+
} else {
|
|
63
|
+
@Suppress("UNCHECKED_CAST")
|
|
64
|
+
(state["pending"] as? ArrayList<View>)?.remove(child)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
override fun setFrame(view: View, x: Double, y: Double, width: Double, height: Double) {
|
|
69
|
+
// The dialog owns its own window; the layout engine frames only the children.
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
override fun teardown(view: View) {
|
|
73
|
+
if (stateOf(view)["dialog"] != null) dismiss(view, fromUser = false)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private fun present(placeholder: View) {
|
|
77
|
+
val state = stateOf(placeholder)
|
|
78
|
+
val props = propsOf(placeholder)
|
|
79
|
+
val ctx = placeholder.context
|
|
80
|
+
val dialog = Dialog(ctx)
|
|
81
|
+
val content = FrameLayout(ctx)
|
|
82
|
+
val presentation = props.str("presentation_style") ?: "page_sheet"
|
|
83
|
+
val isOverlay = presentation == "overlay" || JsonUtil.truthy(props.value("transparent"))
|
|
84
|
+
content.setBackgroundColor(if (isOverlay) 0x00FFFFFF else 0xFFFFFFFF.toInt())
|
|
85
|
+
dialog.setContentView(content, ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
|
|
86
|
+
dialog.window?.let { window ->
|
|
87
|
+
if (isOverlay) {
|
|
88
|
+
window.setBackgroundDrawable(ColorDrawable(0x00000000))
|
|
89
|
+
window.setDimAmount(0.5f)
|
|
90
|
+
window.setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT)
|
|
91
|
+
} else {
|
|
92
|
+
window.setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT)
|
|
93
|
+
}
|
|
94
|
+
val animation = props.str("animation_type") ?: props.str("animation")
|
|
95
|
+
when (animation) {
|
|
96
|
+
"none" -> window.setWindowAnimations(0)
|
|
97
|
+
"fade" -> window.setWindowAnimations(android.R.style.Animation_Toast)
|
|
98
|
+
"slide" -> window.setWindowAnimations(android.R.style.Animation_InputMethod)
|
|
99
|
+
}
|
|
100
|
+
PNColor.parse(props.value("status_bar_color"))?.let { window.statusBarColor = it }
|
|
101
|
+
}
|
|
102
|
+
dialog.setCanceledOnTouchOutside(props.value("dismiss_on_backdrop") != false)
|
|
103
|
+
dialog.setCancelable(props.value("dismissable") != false)
|
|
104
|
+
state["dialog"] = dialog
|
|
105
|
+
state["content_view"] = content
|
|
106
|
+
@Suppress("UNCHECKED_CAST")
|
|
107
|
+
val pending = state.remove("pending") as? ArrayList<View>
|
|
108
|
+
pending?.forEach { child ->
|
|
109
|
+
(child.parent as? ViewGroup)?.removeView(child)
|
|
110
|
+
content.addView(child)
|
|
111
|
+
}
|
|
112
|
+
dialog.setOnShowListener { fire(placeholder, "on_show") }
|
|
113
|
+
dialog.setOnDismissListener {
|
|
114
|
+
// Programmatic dismiss (visible=false) also lands here; the
|
|
115
|
+
// Python side expects on_dismiss only for user-driven closes,
|
|
116
|
+
// but android.py fired it unconditionally, so keep that.
|
|
117
|
+
if (stateOf(placeholder)["dialog"] === dialog) {
|
|
118
|
+
stateOf(placeholder).remove("dialog")
|
|
119
|
+
stateOf(placeholder).remove("content_view")
|
|
120
|
+
}
|
|
121
|
+
fire(placeholder, "on_dismiss")
|
|
122
|
+
if (hasEvent(placeholder, "on_request_close")) fire(placeholder, "on_request_close")
|
|
123
|
+
}
|
|
124
|
+
dialog.show()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private fun dismiss(placeholder: View, fromUser: Boolean) {
|
|
128
|
+
val state = stateOf(placeholder)
|
|
129
|
+
val dialog = state.remove("dialog") as? Dialog
|
|
130
|
+
state.remove("content_view")
|
|
131
|
+
if (!fromUser) dialog?.setOnDismissListener(null)
|
|
132
|
+
dialog?.dismiss()
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
package com.pythonnative.runtime.components
|
|
2
|
+
|
|
3
|
+
import org.json.JSONArray
|
|
4
|
+
import org.json.JSONObject
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Color parsing shared by every manager.
|
|
8
|
+
*
|
|
9
|
+
* Accepts `#RGB`, `#RGBA`, `#RRGGBB`, `#AARRGGBB` (the Android/Python
|
|
10
|
+
* convention used by `parse_color_int`), `rgb(r, g, b)`,
|
|
11
|
+
* `rgba(r, g, b, a)` with `a` in `0..1`, CSS named colors, packed
|
|
12
|
+
* integers (`0xRRGGBB` gains full alpha), and `[r, g, b(, a)]` arrays.
|
|
13
|
+
* Results are signed 32-bit ARGB ints suitable for Android APIs.
|
|
14
|
+
*/
|
|
15
|
+
object PNColor {
|
|
16
|
+
private val named: Map<String, Int> = mapOf(
|
|
17
|
+
"transparent" to 0x00000000,
|
|
18
|
+
"clear" to 0x00000000,
|
|
19
|
+
"black" to 0xFF000000.toInt(),
|
|
20
|
+
"white" to 0xFFFFFFFF.toInt(),
|
|
21
|
+
"red" to 0xFFFF0000.toInt(),
|
|
22
|
+
"green" to 0xFF008000.toInt(),
|
|
23
|
+
"lime" to 0xFF00FF00.toInt(),
|
|
24
|
+
"blue" to 0xFF0000FF.toInt(),
|
|
25
|
+
"yellow" to 0xFFFFFF00.toInt(),
|
|
26
|
+
"cyan" to 0xFF00FFFF.toInt(),
|
|
27
|
+
"aqua" to 0xFF00FFFF.toInt(),
|
|
28
|
+
"magenta" to 0xFFFF00FF.toInt(),
|
|
29
|
+
"fuchsia" to 0xFFFF00FF.toInt(),
|
|
30
|
+
"gray" to 0xFF808080.toInt(),
|
|
31
|
+
"grey" to 0xFF808080.toInt(),
|
|
32
|
+
"darkgray" to 0xFFA9A9A9.toInt(),
|
|
33
|
+
"darkgrey" to 0xFFA9A9A9.toInt(),
|
|
34
|
+
"lightgray" to 0xFFD3D3D3.toInt(),
|
|
35
|
+
"lightgrey" to 0xFFD3D3D3.toInt(),
|
|
36
|
+
"dimgray" to 0xFF696969.toInt(),
|
|
37
|
+
"dimgrey" to 0xFF696969.toInt(),
|
|
38
|
+
"silver" to 0xFFC0C0C0.toInt(),
|
|
39
|
+
"maroon" to 0xFF800000.toInt(),
|
|
40
|
+
"olive" to 0xFF808000.toInt(),
|
|
41
|
+
"navy" to 0xFF000080.toInt(),
|
|
42
|
+
"teal" to 0xFF008080.toInt(),
|
|
43
|
+
"purple" to 0xFF800080.toInt(),
|
|
44
|
+
"orange" to 0xFFFFA500.toInt(),
|
|
45
|
+
"pink" to 0xFFFFC0CB.toInt(),
|
|
46
|
+
"brown" to 0xFFA52A2A.toInt(),
|
|
47
|
+
"gold" to 0xFFFFD700.toInt(),
|
|
48
|
+
"indigo" to 0xFF4B0082.toInt(),
|
|
49
|
+
"violet" to 0xFFEE82EE.toInt(),
|
|
50
|
+
"coral" to 0xFFFF7F50.toInt(),
|
|
51
|
+
"salmon" to 0xFFFA8072.toInt(),
|
|
52
|
+
"tomato" to 0xFFFF6347.toInt(),
|
|
53
|
+
"crimson" to 0xFFDC143C.toInt(),
|
|
54
|
+
"khaki" to 0xFFF0E68C.toInt(),
|
|
55
|
+
"beige" to 0xFFF5F5DC.toInt(),
|
|
56
|
+
"ivory" to 0xFFFFFFF0.toInt(),
|
|
57
|
+
"tan" to 0xFFD2B48C.toInt(),
|
|
58
|
+
"turquoise" to 0xFF40E0D0.toInt(),
|
|
59
|
+
"skyblue" to 0xFF87CEEB.toInt(),
|
|
60
|
+
"steelblue" to 0xFF4682B4.toInt(),
|
|
61
|
+
"royalblue" to 0xFF4169E1.toInt(),
|
|
62
|
+
"dodgerblue" to 0xFF1E90FF.toInt(),
|
|
63
|
+
"slategray" to 0xFF708090.toInt(),
|
|
64
|
+
"slategrey" to 0xFF708090.toInt(),
|
|
65
|
+
"whitesmoke" to 0xFFF5F5F5.toInt(),
|
|
66
|
+
"snow" to 0xFFFFFAFA.toInt(),
|
|
67
|
+
"seagreen" to 0xFF2E8B57.toInt(),
|
|
68
|
+
"forestgreen" to 0xFF228B22.toInt(),
|
|
69
|
+
"darkgreen" to 0xFF006400.toInt(),
|
|
70
|
+
"lightblue" to 0xFFADD8E6.toInt(),
|
|
71
|
+
"lightgreen" to 0xFF90EE90.toInt(),
|
|
72
|
+
"darkblue" to 0xFF00008B.toInt(),
|
|
73
|
+
"darkred" to 0xFF8B0000.toInt(),
|
|
74
|
+
"orangered" to 0xFFFF4500.toInt(),
|
|
75
|
+
"hotpink" to 0xFFFF69B4.toInt(),
|
|
76
|
+
"deeppink" to 0xFFFF1493.toInt(),
|
|
77
|
+
"chocolate" to 0xFFD2691E.toInt(),
|
|
78
|
+
"plum" to 0xFFDDA0DD.toInt(),
|
|
79
|
+
"lavender" to 0xFFE6E6FA.toInt(),
|
|
80
|
+
"mintcream" to 0xFFF5FFFA.toInt(),
|
|
81
|
+
"gainsboro" to 0xFFDCDCDC.toInt(),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
/** Parse `value` into a signed ARGB int, or `null` when unparseable. */
|
|
85
|
+
fun parse(value: Any?): Int? {
|
|
86
|
+
return when (value) {
|
|
87
|
+
null, JSONObject.NULL -> null
|
|
88
|
+
is Boolean -> null
|
|
89
|
+
is Int -> if (value ushr 24 == 0) value or 0xFF000000.toInt() else value
|
|
90
|
+
is Long -> parseLong(value)
|
|
91
|
+
is Number -> {
|
|
92
|
+
val d = value.toDouble()
|
|
93
|
+
if (d.isNaN() || d.isInfinite()) null else parseLong(d.toLong())
|
|
94
|
+
}
|
|
95
|
+
is String -> parseString(value)
|
|
96
|
+
is JSONArray -> parseArray((0 until value.length()).map { value.opt(it) })
|
|
97
|
+
is List<*> -> parseArray(value)
|
|
98
|
+
else -> null
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Parse `value`, falling back to `default` when unparseable. */
|
|
103
|
+
fun parseOr(value: Any?, default: Int): Int = parse(value) ?: default
|
|
104
|
+
|
|
105
|
+
/** Replace the alpha channel of `color` with `alpha` in `0..1`. */
|
|
106
|
+
fun withAlpha(color: Int, alpha: Double): Int {
|
|
107
|
+
val a = (alpha.coerceIn(0.0, 1.0) * 255.0).toInt()
|
|
108
|
+
return (color and 0x00FFFFFF) or (a shl 24)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private fun parseLong(raw: Long): Int {
|
|
112
|
+
val masked = raw and 0xFFFFFFFFL
|
|
113
|
+
val withAlpha = if (masked ushr 24 == 0L) masked or 0xFF000000L else masked
|
|
114
|
+
return withAlpha.toInt()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private fun parseArray(items: List<Any?>): Int? {
|
|
118
|
+
if (items.size < 3) return null
|
|
119
|
+
val r = channel(items[0]) ?: return null
|
|
120
|
+
val g = channel(items[1]) ?: return null
|
|
121
|
+
val b = channel(items[2]) ?: return null
|
|
122
|
+
val a = if (items.size >= 4) alphaChannel(items[3]) ?: return null else 255
|
|
123
|
+
return argb(a, r, g, b)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private fun parseString(raw: String): Int? {
|
|
127
|
+
val s = raw.trim()
|
|
128
|
+
if (s.isEmpty()) return null
|
|
129
|
+
if (s.startsWith("#")) return parseHex(s.substring(1))
|
|
130
|
+
val lower = s.lowercase()
|
|
131
|
+
named[lower]?.let { return it }
|
|
132
|
+
if (lower.startsWith("rgb")) return parseFunctional(lower)
|
|
133
|
+
if (lower.startsWith("0x")) return s.substring(2).toLongOrNull(16)?.let { parseLong(it) }
|
|
134
|
+
return s.toLongOrNull(16)?.takeIf { s.length == 6 || s.length == 8 }?.let {
|
|
135
|
+
parseHexDigits(s)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private fun parseHex(digits: String): Int? {
|
|
140
|
+
if (digits.isEmpty() || !digits.all { it.isLetterOrDigit() }) return null
|
|
141
|
+
val expanded = when (digits.length) {
|
|
142
|
+
3 -> "FF" + digits.map { "$it$it" }.joinToString("")
|
|
143
|
+
4 -> {
|
|
144
|
+
// CSS #RGBA: alpha is the last digit.
|
|
145
|
+
val rgb = digits.substring(0, 3).map { "$it$it" }.joinToString("")
|
|
146
|
+
val a = "${digits[3]}${digits[3]}"
|
|
147
|
+
a + rgb
|
|
148
|
+
}
|
|
149
|
+
6 -> "FF$digits"
|
|
150
|
+
8 -> digits
|
|
151
|
+
else -> return null
|
|
152
|
+
}
|
|
153
|
+
return expanded.toLongOrNull(16)?.toInt()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private fun parseHexDigits(digits: String): Int? = parseHex(digits)
|
|
157
|
+
|
|
158
|
+
private fun parseFunctional(lower: String): Int? {
|
|
159
|
+
val open = lower.indexOf('(')
|
|
160
|
+
val close = lower.lastIndexOf(')')
|
|
161
|
+
if (open < 0 || close < open) return null
|
|
162
|
+
val parts = lower.substring(open + 1, close).split(',', '/', ' ').map { it.trim() }.filter { it.isNotEmpty() }
|
|
163
|
+
if (parts.size < 3) return null
|
|
164
|
+
val r = channel(parts[0]) ?: return null
|
|
165
|
+
val g = channel(parts[1]) ?: return null
|
|
166
|
+
val b = channel(parts[2]) ?: return null
|
|
167
|
+
val a = if (parts.size >= 4) alphaChannel(parts[3]) ?: return null else 255
|
|
168
|
+
return argb(a, r, g, b)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private fun channel(value: Any?): Int? {
|
|
172
|
+
val d = when (value) {
|
|
173
|
+
is Number -> value.toDouble()
|
|
174
|
+
is String -> {
|
|
175
|
+
val t = value.trim()
|
|
176
|
+
if (t.endsWith("%")) (t.dropLast(1).toDoubleOrNull() ?: return null) * 2.55
|
|
177
|
+
else t.toDoubleOrNull() ?: return null
|
|
178
|
+
}
|
|
179
|
+
else -> return null
|
|
180
|
+
}
|
|
181
|
+
return d.toInt().coerceIn(0, 255)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private fun alphaChannel(value: Any?): Int? {
|
|
185
|
+
val d = when (value) {
|
|
186
|
+
is Number -> value.toDouble()
|
|
187
|
+
is String -> {
|
|
188
|
+
val t = value.trim()
|
|
189
|
+
if (t.endsWith("%")) (t.dropLast(1).toDoubleOrNull() ?: return null) / 100.0
|
|
190
|
+
else t.toDoubleOrNull() ?: return null
|
|
191
|
+
}
|
|
192
|
+
else -> return null
|
|
193
|
+
}
|
|
194
|
+
// Values above 1 are taken as 0..255 bytes.
|
|
195
|
+
val a = if (d > 1.0) d / 255.0 else d
|
|
196
|
+
return (a.coerceIn(0.0, 1.0) * 255.0 + 0.5).toInt()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private fun argb(a: Int, r: Int, g: Int, b: Int): Int =
|
|
200
|
+
(a shl 24) or (r shl 16) or (g shl 8) or b
|
|
201
|
+
}
|