craft-native 0.0.173 → 0.0.174

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.
@@ -52,6 +52,8 @@ export interface CraftAndroidConfig {
52
52
  trustedOrigins?: string[];
53
53
  appIconPath?: string;
54
54
  googleServicesFile?: string;
55
+ /** Google Maps Android SDK key used by native `Map` screens. */
56
+ mapsApiKey?: string;
55
57
  devServerURL?: string;
56
58
  hasBundledFallback?: boolean;
57
59
  /** Android API level, at least ANDROID_MIN_SDK (26). */
@@ -271,6 +271,9 @@ function validateAndroidConfig(config) {
271
271
  if (config.backgroundColorDark !== undefined && !config.backgroundColorDark.match(/^#[\dA-F]{3,4}$|^#[\dA-F]{6}$|^#[\dA-F]{8}$/i)) {
272
272
  throw new Error(`Invalid Android dark background color: ${config.backgroundColorDark}`);
273
273
  }
274
+ if (config.mapsApiKey !== undefined && !config.mapsApiKey.trim()) {
275
+ throw new Error("Android mapsApiKey must not be empty");
276
+ }
274
277
  for (const [name, value] of [
275
278
  ["versionCode", config.versionCode],
276
279
  ["minSdk", config.minSdk],
@@ -570,7 +573,7 @@ async function init(options) {
570
573
  const healthTemplate = readFileSync(join(TEMPLATES_DIR, config.enableHealthConnect ? "CraftHealthConnect.kt.template" : "CraftHealthConnectStub.kt.template"), "utf-8");
571
574
  writeFileSync(join(output, "app/src/main/java", packagePath, "CraftHealthConnect.kt"), healthTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
572
575
  const manifestTemplate = readFileSync(join(TEMPLATES_DIR, "AndroidManifest.xml.template"), "utf-8");
573
- const manifest = manifestTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{PERMISSIONS\}\}/g, renderAndroidPermissions(config)).replace(/\{\{USES_CLEARTEXT\}\}/g, config.devServerURL?.startsWith("http://") || config.enableLocalNetwork ? "true" : "false").replace(/\{\{DEEP_LINK_INTENT_FILTERS\}\}/g, renderAndroidDeepLinks(config)).replace(/\{\{BACKGROUND_SERVICE\}\}/g, config.enableBackgroundLocation ? ' <service android:name="com.craft.runtime.LocationRecordingService" android:exported="false" android:foregroundServiceType="location" android:stopWithTask="false" />' : "").replace(/\{\{NATIVE_NOTIFICATION_RECEIVER\}\}/g, config.renderer === "native" ? renderNativeShellManifest(config) : "").replace(/\{\{HEALTH_CONNECT_QUERIES\}\}/g, config.enableHealthConnect ? ` <queries>
576
+ const manifest = manifestTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{PERMISSIONS\}\}/g, renderAndroidPermissions(config)).replace(/\{\{USES_CLEARTEXT\}\}/g, config.devServerURL?.startsWith("http://") || config.enableLocalNetwork ? "true" : "false").replace(/\{\{DEEP_LINK_INTENT_FILTERS\}\}/g, renderAndroidDeepLinks(config)).replace(/\{\{BACKGROUND_SERVICE\}\}/g, config.enableBackgroundLocation ? ' <service android:name="com.craft.runtime.LocationRecordingService" android:exported="false" android:foregroundServiceType="location" android:stopWithTask="false" />' : "").replace(/\{\{NATIVE_NOTIFICATION_RECEIVER\}\}/g, config.renderer === "native" ? renderNativeShellManifest(config) : "").replace(/\{\{GOOGLE_MAPS_API_KEY\}\}/g, config.mapsApiKey ? ` <meta-data android:name="com.google.android.geo.API_KEY" android:value="${escapeXml(config.mapsApiKey)}" />` : "").replace(/\{\{HEALTH_CONNECT_QUERIES\}\}/g, config.enableHealthConnect ? ` <queries>
574
577
  <package android:name="com.google.android.apps.healthdata" />
575
578
  </queries>` : "").replace(/\{\{HEALTH_CONNECT_RATIONALE\}\}/g, config.enableHealthConnect ? ` <intent-filter>
576
579
  <action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
@@ -30,6 +30,8 @@
30
30
  android:hardwareAccelerated="true"
31
31
  tools:targetApi="34">
32
32
 
33
+ {{GOOGLE_MAPS_API_KEY}}
34
+
33
35
  <activity
34
36
  android:name=".MainActivity"
35
37
  android:exported="true"
@@ -13,8 +13,10 @@ import android.text.TextUtils
13
13
  import android.util.Log
14
14
  import android.util.TypedValue
15
15
  import android.view.Gravity
16
+ import android.view.MotionEvent
16
17
  import android.view.View
17
18
  import android.view.ViewGroup
19
+ import android.view.ViewConfiguration
18
20
  import android.widget.FrameLayout
19
21
  import android.widget.ImageView
20
22
  import android.widget.LinearLayout
@@ -576,10 +578,19 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
576
578
  private val tabViews = mutableListOf<Pair<TextView, TextView>>()
577
579
  private val density = resources.displayMetrics.density
578
580
  private var selectedTab = 0
581
+ private var swipeTracking = false
582
+ private var swipeIntercepted = false
583
+ private var swipeDownX = 0f
584
+ private var swipeDownY = 0f
585
+ private val swipeSlop = ViewConfiguration.get(context).scaledTouchSlop
579
586
 
580
587
  var onBack: (() -> Unit)? = null
581
588
  var onButton: ((String) -> Unit)? = null
582
589
  var onTab: ((Int) -> Unit)? = null
590
+ var onTabSwipeStart: ((Float, Float, Float) -> Boolean)? = null
591
+ var onTabSwipeMove: ((Float) -> Unit)? = null
592
+ var onTabSwipeEnd: ((Float, Boolean) -> Unit)? = null
593
+ var tabSwipeEnabled = true
583
594
 
584
595
  init {
585
596
  orientation = VERTICAL
@@ -640,6 +651,44 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
640
651
  refreshColors()
641
652
  }
642
653
 
654
+ override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
655
+ when (event.actionMasked) {
656
+ MotionEvent.ACTION_DOWN -> {
657
+ swipeTracking = tabs.size > 1 && tabSwipeEnabled && tabBar.visibility == View.VISIBLE && event.y < tabBar.top
658
+ swipeIntercepted = false
659
+ swipeDownX = event.x
660
+ swipeDownY = event.y
661
+ }
662
+ MotionEvent.ACTION_MOVE -> if (swipeTracking && !swipeIntercepted) {
663
+ val dx = event.x - swipeDownX
664
+ val dy = event.y - swipeDownY
665
+ if (kotlin.math.abs(dx) > swipeSlop && kotlin.math.abs(dx) > kotlin.math.abs(dy)) {
666
+ swipeTracking = false
667
+ swipeIntercepted = onTabSwipeStart?.invoke(dx, event.x, event.y) == true
668
+ return swipeIntercepted
669
+ }
670
+ }
671
+ MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> swipeTracking = false
672
+ }
673
+ return swipeIntercepted
674
+ }
675
+
676
+ override fun onTouchEvent(event: MotionEvent): Boolean {
677
+ if (!swipeIntercepted) return super.onTouchEvent(event)
678
+ when (event.actionMasked) {
679
+ MotionEvent.ACTION_MOVE -> onTabSwipeMove?.invoke(event.x - swipeDownX)
680
+ MotionEvent.ACTION_UP -> {
681
+ onTabSwipeEnd?.invoke(event.x - swipeDownX, false)
682
+ swipeIntercepted = false
683
+ }
684
+ MotionEvent.ACTION_CANCEL -> {
685
+ onTabSwipeEnd?.invoke(event.x - swipeDownX, true)
686
+ swipeIntercepted = false
687
+ }
688
+ }
689
+ return true
690
+ }
691
+
643
692
  /** Draws `options` (a screen's `setOptions`) for a screen `depth` deep in its stack. */
644
693
  fun apply(options: JSONObject, depth: Int, fallbackTitle: String?) {
645
694
  val hidden = options.optBoolean("hidden", false)
@@ -659,6 +708,7 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
659
708
  }
660
709
  val tabBarHidden = options.optBoolean("tabBarHidden", false) || depth > 0 && options.optBoolean("tabBarHiddenWhenPushed", false)
661
710
  tabBar.visibility = if (tabs.isEmpty() || tabBarHidden) View.GONE else View.VISIBLE
711
+ tabSwipeEnabled = options.optBoolean("tabSwipeEnabled", true)
662
712
  }
663
713
 
664
714
  fun selectTab(index: Int) {