nn-widgets 0.1.16 → 0.1.18

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nn-widgets",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "Expo config plugin for adding native widgets (iOS WidgetKit & Android App Widgets)",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -1 +1 @@
1
- {"version":3,"file":"withAndroidWidget.d.ts","sourceRoot":"","sources":["../src/withAndroidWidget.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,sBAAsB,CAAC;AAGzE,OAAO,KAAK,EACV,oBAAoB,EAGrB,MAAM,SAAS,CAAC;AA+hBjB,eAAO,MAAM,iBAAiB,EAAE,YAAY,CAAC,oBAAoB,CAuNhE,CAAC;AAEF,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"withAndroidWidget.d.ts","sourceRoot":"","sources":["../src/withAndroidWidget.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,sBAAsB,CAAC;AAGzE,OAAO,KAAK,EACV,oBAAoB,EAGrB,MAAM,SAAS,CAAC;AAmgCjB,eAAO,MAAM,iBAAiB,EAAE,YAAY,CAAC,oBAAoB,CAyQhE,CAAC;AAEF,eAAe,iBAAiB,CAAC"}
@@ -39,6 +39,38 @@ const fs = __importStar(require("fs"));
39
39
  const path = __importStar(require("path"));
40
40
  const withIosWidget_1 = require("./withIosWidget");
41
41
  // ──────────────────────────────────────────────
42
+ // Constants and helpers
43
+ // ──────────────────────────────────────────────
44
+ // Material background names that should use blur effect on Android 12+
45
+ const BLUR_BACKGROUNDS = [
46
+ "blur",
47
+ "ultraThinMaterial",
48
+ "thinMaterial",
49
+ "regularMaterial",
50
+ "thickMaterial",
51
+ "ultraThickMaterial",
52
+ ];
53
+ /**
54
+ * Check if backgroundColor should use blur effect
55
+ */
56
+ function isBlurBackground(bgColor) {
57
+ return bgColor ? BLUR_BACKGROUNDS.includes(bgColor) : false;
58
+ }
59
+ /**
60
+ * Resolve Android background attribute for XML.
61
+ * - If blur/material: use system widget background (API 31+) or transparent
62
+ * - Otherwise: use the color directly
63
+ */
64
+ function resolveAndroidBackground(bgColor) {
65
+ if (!bgColor)
66
+ return "#FFFFFF";
67
+ if (isBlurBackground(bgColor)) {
68
+ // Will be handled by drawable resource
69
+ return "@drawable/widget_blur_background";
70
+ }
71
+ return bgColor;
72
+ }
73
+ // ──────────────────────────────────────────────
42
74
  // Code generation helpers (per widget)
43
75
  // ──────────────────────────────────────────────
44
76
  function generateWidgetProviderCode(w, packageName, deepLinkUrl) {
@@ -286,6 +318,264 @@ class ${w.name}Provider : AppWidgetProvider() {
286
318
  }
287
319
  }
288
320
  }
321
+ `;
322
+ }
323
+ // ── Flex-grid type provider ──
324
+ if (w.type === "flex-grid") {
325
+ const bgColor = w.style?.backgroundColor || "#FFFFFF";
326
+ const titleColor = w.style?.titleColor
327
+ ? `android.graphics.Color.parseColor("${w.style.titleColor}")`
328
+ : "android.graphics.Color.BLACK";
329
+ const subtitleColor = w.style?.subtitleColor
330
+ ? `android.graphics.Color.parseColor("${w.style.subtitleColor}")`
331
+ : "android.graphics.Color.GRAY";
332
+ const accentColor = w.style?.accentColor
333
+ ? `android.graphics.Color.parseColor("${w.style.accentColor}")`
334
+ : 'android.graphics.Color.parseColor("#6200EE")';
335
+ return `package ${packageName}.widget
336
+
337
+ import android.app.PendingIntent
338
+ import android.appwidget.AppWidgetManager
339
+ import android.appwidget.AppWidgetProvider
340
+ import android.content.Context
341
+ import android.content.Intent
342
+ import android.view.View
343
+ import android.widget.RemoteViews
344
+ import ${packageName}.MainActivity
345
+ import ${packageName}.R
346
+ import org.json.JSONArray
347
+ import org.json.JSONObject
348
+
349
+ /**
350
+ * ${w.name} - Flex Grid Widget Provider
351
+ * Auto-generated by nn-widgets
352
+ */
353
+ class ${w.name}Provider : AppWidgetProvider() {
354
+
355
+ companion object {
356
+ const val PREFS_NAME = "nn_widgets_data"
357
+ const val MAX_ROWS = 4
358
+ const val MAX_COLS = 4
359
+ }
360
+
361
+ override fun onUpdate(
362
+ context: Context,
363
+ appWidgetManager: AppWidgetManager,
364
+ appWidgetIds: IntArray
365
+ ) {
366
+ for (appWidgetId in appWidgetIds) {
367
+ updateAppWidget(context, appWidgetManager, appWidgetId)
368
+ }
369
+ }
370
+
371
+ override fun onEnabled(context: Context) {}
372
+ override fun onDisabled(context: Context) {}
373
+
374
+ override fun onReceive(context: Context, intent: Intent) {
375
+ super.onReceive(context, intent)
376
+ if (intent.action == "\${context.packageName}.WIDGET_UPDATE_${w.name.toUpperCase()}") {
377
+ val appWidgetManager = AppWidgetManager.getInstance(context)
378
+ val thisWidget = android.content.ComponentName(context, ${w.name}Provider::class.java)
379
+ val appWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget)
380
+ onUpdate(context, appWidgetManager, appWidgetIds)
381
+ }
382
+ }
383
+
384
+ private fun updateAppWidget(
385
+ context: Context,
386
+ appWidgetManager: AppWidgetManager,
387
+ appWidgetId: Int
388
+ ) {
389
+ val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
390
+ val views = RemoteViews(context.packageName, R.layout.${w.name.toLowerCase()}_layout)
391
+
392
+ // Read items and layout JSON (same API as iOS)
393
+ val itemsJson = prefs.getString("widget_${w.name}_items", null)
394
+ val layoutJson = prefs.getString("widget_${w.name}_layout", null)
395
+ val title = prefs.getString("widget_${w.name}_title", "${w.fallbackTitle}") ?: "${w.fallbackTitle}"
396
+ val subtitle = prefs.getString("widget_${w.name}_subtitle", "${w.fallbackSubtitle}") ?: "${w.fallbackSubtitle}"
397
+
398
+ if (itemsJson != null && layoutJson != null) {
399
+ try {
400
+ val items = JSONArray(itemsJson)
401
+ val layoutObj = JSONObject(layoutJson)
402
+ // Get layout for systemSmall (default) - same as iOS
403
+ val layout = layoutObj.optJSONArray("systemSmall") ?: JSONArray("[[1]]")
404
+
405
+ // Hide fallback, show grid
406
+ views.setViewVisibility(R.id.widget_fallback, View.GONE)
407
+ views.setViewVisibility(R.id.widget_grid_container, View.VISIBLE)
408
+
409
+ // Clear existing rows
410
+ views.removeAllViews(R.id.widget_grid_container)
411
+
412
+ var itemIndex = 0
413
+ val rowCount = minOf(layout.length(), MAX_ROWS)
414
+
415
+ for (rowIdx in 0 until rowCount) {
416
+ val rowRatios = layout.optJSONArray(rowIdx) ?: continue
417
+ val colCount = minOf(rowRatios.length(), MAX_COLS)
418
+
419
+ // Create row container
420
+ val rowView = RemoteViews(context.packageName, R.layout.${w.name.toLowerCase()}_row)
421
+
422
+ for (colIdx in 0 until colCount) {
423
+ val ratio = rowRatios.optDouble(colIdx, 1.0)
424
+ val isListStyle = ratio >= 1.0
425
+
426
+ if (itemIndex < items.length()) {
427
+ val item = items.getJSONObject(itemIndex)
428
+ val cellView = if (isListStyle) {
429
+ createListStyleCell(context, item, itemIndex)
430
+ } else {
431
+ createCardStyleCell(context, item, itemIndex)
432
+ }
433
+ rowView.addView(R.id.row_container, cellView)
434
+ }
435
+ itemIndex++
436
+ }
437
+
438
+ views.addView(R.id.widget_grid_container, rowView)
439
+ }
440
+ } catch (e: Exception) {
441
+ android.util.Log.e("${w.name}Provider", "Error parsing widget data", e)
442
+ showFallback(views, title, subtitle)
443
+ }
444
+ } else {
445
+ showFallback(views, title, subtitle)
446
+ }
447
+
448
+ // Widget-level click
449
+ ${intentCode}
450
+ val pendingIntent = PendingIntent.getActivity(
451
+ context, 100, intent,
452
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
453
+ )
454
+ views.setOnClickPendingIntent(R.id.widget_container, pendingIntent)
455
+
456
+ appWidgetManager.updateAppWidget(appWidgetId, views)
457
+ }
458
+
459
+ private fun createListStyleCell(context: Context, item: JSONObject, index: Int): RemoteViews {
460
+ val cellView = RemoteViews(context.packageName, R.layout.${w.name.toLowerCase()}_cell_list)
461
+
462
+ // Parse title
463
+ val itemTitle = parseTextValue(item, "title")
464
+ cellView.setTextViewText(R.id.cell_title, itemTitle)
465
+ cellView.setTextColor(R.id.cell_title, ${titleColor})
466
+
467
+ // Parse description
468
+ val itemDesc = parseTextValue(item, "description")
469
+ if (itemDesc.isNotEmpty()) {
470
+ cellView.setTextViewText(R.id.cell_description, itemDesc)
471
+ cellView.setTextColor(R.id.cell_description, ${subtitleColor})
472
+ cellView.setViewVisibility(R.id.cell_description, View.VISIBLE)
473
+ } else {
474
+ cellView.setViewVisibility(R.id.cell_description, View.GONE)
475
+ }
476
+
477
+ // Parse icon
478
+ setupIcon(context, cellView, item, R.id.cell_icon)
479
+
480
+ // Deep link
481
+ setupDeepLink(context, cellView, item, index, R.id.cell_container)
482
+
483
+ return cellView
484
+ }
485
+
486
+ private fun createCardStyleCell(context: Context, item: JSONObject, index: Int): RemoteViews {
487
+ val cellView = RemoteViews(context.packageName, R.layout.${w.name.toLowerCase()}_cell_card)
488
+
489
+ // Parse title (smaller for card style)
490
+ val itemTitle = parseTextValue(item, "title")
491
+ cellView.setTextViewText(R.id.cell_title, itemTitle)
492
+ cellView.setTextColor(R.id.cell_title, ${titleColor})
493
+
494
+ // Parse icon (larger for card style)
495
+ setupIcon(context, cellView, item, R.id.cell_icon)
496
+
497
+ // Deep link
498
+ setupDeepLink(context, cellView, item, index, R.id.cell_container)
499
+
500
+ return cellView
501
+ }
502
+
503
+ private fun setupIcon(context: Context, views: RemoteViews, item: JSONObject, iconViewId: Int) {
504
+ val iconName = parseIconName(item)
505
+ if (iconName.isNotEmpty()) {
506
+ // Try to load from drawable resources
507
+ val resId = context.resources.getIdentifier(
508
+ iconName.replace("-", "_").lowercase(),
509
+ "drawable",
510
+ context.packageName
511
+ )
512
+ if (resId != 0) {
513
+ views.setImageViewResource(iconViewId, resId)
514
+ views.setViewVisibility(iconViewId, View.VISIBLE)
515
+ } else {
516
+ // Fallback to a default icon or hide
517
+ views.setViewVisibility(iconViewId, View.GONE)
518
+ }
519
+ } else {
520
+ views.setViewVisibility(iconViewId, View.GONE)
521
+ }
522
+ }
523
+
524
+ private fun setupDeepLink(context: Context, views: RemoteViews, item: JSONObject, index: Int, containerId: Int) {
525
+ val deepLink = item.optString("deepLink", "")
526
+ if (deepLink.isNotEmpty()) {
527
+ val itemIntent = Intent(Intent.ACTION_VIEW, android.net.Uri.parse(deepLink)).apply {
528
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
529
+ }
530
+ val pendingIntent = PendingIntent.getActivity(
531
+ context, index, itemIntent,
532
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
533
+ )
534
+ views.setOnClickPendingIntent(containerId, pendingIntent)
535
+ }
536
+ }
537
+
538
+ private fun showFallback(views: RemoteViews, title: String, subtitle: String) {
539
+ views.setViewVisibility(R.id.widget_fallback, View.VISIBLE)
540
+ views.setViewVisibility(R.id.widget_grid_container, View.GONE)
541
+ views.setTextViewText(R.id.fallback_title, title)
542
+ views.setTextColor(R.id.fallback_title, ${titleColor})
543
+ views.setTextViewText(R.id.fallback_subtitle, subtitle)
544
+ views.setTextColor(R.id.fallback_subtitle, ${subtitleColor})
545
+ }
546
+
547
+ private fun parseTextValue(item: JSONObject, key: String): String {
548
+ return when {
549
+ item.has(key) && item.get(key) is String -> item.getString(key)
550
+ item.has(key) && item.get(key) is JSONObject -> item.getJSONObject(key).optString("text", "")
551
+ else -> ""
552
+ }
553
+ }
554
+
555
+ private fun parseIconName(item: JSONObject): String {
556
+ if (!item.has("icon")) return ""
557
+ return when {
558
+ item.get("icon") is String -> {
559
+ val iconStr = item.getString("icon")
560
+ // Extract filename from URL if it's a URL
561
+ if (iconStr.startsWith("http")) {
562
+ iconStr.substringAfterLast("/").substringBeforeLast(".")
563
+ } else {
564
+ iconStr
565
+ }
566
+ }
567
+ item.get("icon") is JSONObject -> {
568
+ val iconUrl = item.getJSONObject("icon").optString("url", "")
569
+ if (iconUrl.startsWith("http")) {
570
+ iconUrl.substringAfterLast("/").substringBeforeLast(".")
571
+ } else {
572
+ iconUrl
573
+ }
574
+ }
575
+ else -> ""
576
+ }
577
+ }
578
+ }
289
579
  `;
290
580
  }
291
581
  // ── Single-type provider (default) ──
@@ -388,7 +678,7 @@ function generateWidgetLayoutXml(w) {
388
678
  }
389
679
  // ── List layout ──
390
680
  if (w.type === "list") {
391
- const bgColor = w.style?.backgroundColor || "#FFFFFF";
681
+ const bgColor = resolveAndroidBackground(w.style?.backgroundColor);
392
682
  const titleColor = w.style?.titleColor || "#000000";
393
683
  const subtitleColor = w.style?.subtitleColor || "#888888";
394
684
  return `<?xml version="1.0" encoding="utf-8"?>
@@ -436,11 +726,64 @@ function generateWidgetLayoutXml(w) {
436
726
  android:orientation="vertical"
437
727
  android:visibility="gone" />
438
728
 
729
+ </LinearLayout>
730
+ `;
731
+ }
732
+ // ── Flex-grid layout ──
733
+ if (w.type === "flex-grid") {
734
+ const bgColor = resolveAndroidBackground(w.style?.backgroundColor);
735
+ const titleColor = w.style?.titleColor || "#000000";
736
+ const subtitleColor = w.style?.subtitleColor || "#888888";
737
+ return `<?xml version="1.0" encoding="utf-8"?>
738
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
739
+ android:id="@+id/widget_container"
740
+ android:layout_width="match_parent"
741
+ android:layout_height="match_parent"
742
+ android:orientation="vertical"
743
+ android:padding="8dp"
744
+ android:background="${bgColor}">
745
+
746
+ <!-- Fallback view (shown when no data) -->
747
+ <LinearLayout
748
+ android:id="@+id/widget_fallback"
749
+ android:layout_width="match_parent"
750
+ android:layout_height="match_parent"
751
+ android:orientation="vertical"
752
+ android:gravity="center"
753
+ android:visibility="visible">
754
+
755
+ <TextView
756
+ android:id="@+id/fallback_title"
757
+ android:layout_width="wrap_content"
758
+ android:layout_height="wrap_content"
759
+ android:text="${w.fallbackTitle}"
760
+ android:textSize="16sp"
761
+ android:textStyle="bold"
762
+ android:textColor="${titleColor}" />
763
+
764
+ <TextView
765
+ android:id="@+id/fallback_subtitle"
766
+ android:layout_width="wrap_content"
767
+ android:layout_height="wrap_content"
768
+ android:layout_marginTop="4dp"
769
+ android:text="${w.fallbackSubtitle}"
770
+ android:textSize="12sp"
771
+ android:textColor="${subtitleColor}" />
772
+ </LinearLayout>
773
+
774
+ <!-- Grid container (populated dynamically with rows) -->
775
+ <LinearLayout
776
+ android:id="@+id/widget_grid_container"
777
+ android:layout_width="match_parent"
778
+ android:layout_height="match_parent"
779
+ android:orientation="vertical"
780
+ android:visibility="gone" />
781
+
439
782
  </LinearLayout>
440
783
  `;
441
784
  }
442
785
  // ── Single (default) styled text layout ──
443
- const bgColor = w.style?.backgroundColor || "#FFFFFF";
786
+ const bgColor = resolveAndroidBackground(w.style?.backgroundColor);
444
787
  const titleColor = w.style?.titleColor || "#000000";
445
788
  const subtitleColor = w.style?.subtitleColor || "#888888";
446
789
  const accentColor = w.style?.accentColor || "#6200EE";
@@ -536,6 +879,133 @@ function generateWidgetItemLayoutXml(w) {
536
879
  </LinearLayout>
537
880
  `;
538
881
  }
882
+ /** Generate row layout XML for flex-grid widgets */
883
+ function generateFlexGridRowLayoutXml(w) {
884
+ return `<?xml version="1.0" encoding="utf-8"?>
885
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
886
+ android:id="@+id/row_container"
887
+ android:layout_width="match_parent"
888
+ android:layout_height="0dp"
889
+ android:layout_weight="1"
890
+ android:orientation="horizontal"
891
+ android:gravity="center" />
892
+ `;
893
+ }
894
+ /** Generate list-style cell layout XML for flex-grid widgets */
895
+ function generateFlexGridCellListLayoutXml(w) {
896
+ const titleColor = w.style?.titleColor || "#000000";
897
+ const subtitleColor = w.style?.subtitleColor || "#888888";
898
+ const accentColor = w.style?.accentColor || "#6200EE";
899
+ return `<?xml version="1.0" encoding="utf-8"?>
900
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
901
+ android:id="@+id/cell_container"
902
+ android:layout_width="0dp"
903
+ android:layout_height="match_parent"
904
+ android:layout_weight="1"
905
+ android:orientation="horizontal"
906
+ android:gravity="center_vertical"
907
+ android:paddingHorizontal="6dp"
908
+ android:paddingVertical="4dp">
909
+
910
+ <ImageView
911
+ android:id="@+id/cell_icon"
912
+ android:layout_width="36dp"
913
+ android:layout_height="36dp"
914
+ android:layout_marginEnd="8dp"
915
+ android:scaleType="centerCrop"
916
+ android:visibility="gone" />
917
+
918
+ <LinearLayout
919
+ android:layout_width="0dp"
920
+ android:layout_height="wrap_content"
921
+ android:layout_weight="1"
922
+ android:orientation="vertical">
923
+
924
+ <TextView
925
+ android:id="@+id/cell_title"
926
+ android:layout_width="match_parent"
927
+ android:layout_height="wrap_content"
928
+ android:textSize="13sp"
929
+ android:textStyle="bold"
930
+ android:textColor="${titleColor}"
931
+ android:maxLines="1"
932
+ android:ellipsize="end" />
933
+
934
+ <TextView
935
+ android:id="@+id/cell_description"
936
+ android:layout_width="match_parent"
937
+ android:layout_height="wrap_content"
938
+ android:textSize="11sp"
939
+ android:textColor="${subtitleColor}"
940
+ android:maxLines="1"
941
+ android:ellipsize="end"
942
+ android:visibility="gone" />
943
+ </LinearLayout>
944
+
945
+ </LinearLayout>
946
+ `;
947
+ }
948
+ /** Generate card-style cell layout XML for flex-grid widgets */
949
+ function generateFlexGridCellCardLayoutXml(w) {
950
+ const titleColor = w.style?.titleColor || "#000000";
951
+ const accentColor = w.style?.accentColor || "#6200EE";
952
+ return `<?xml version="1.0" encoding="utf-8"?>
953
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
954
+ android:id="@+id/cell_container"
955
+ android:layout_width="0dp"
956
+ android:layout_height="match_parent"
957
+ android:layout_weight="1"
958
+ android:orientation="vertical"
959
+ android:gravity="center"
960
+ android:padding="4dp">
961
+
962
+ <ImageView
963
+ android:id="@+id/cell_icon"
964
+ android:layout_width="40dp"
965
+ android:layout_height="40dp"
966
+ android:scaleType="centerCrop"
967
+ android:visibility="gone" />
968
+
969
+ <TextView
970
+ android:id="@+id/cell_title"
971
+ android:layout_width="match_parent"
972
+ android:layout_height="wrap_content"
973
+ android:layout_marginTop="4dp"
974
+ android:textSize="10sp"
975
+ android:textColor="${titleColor}"
976
+ android:maxLines="2"
977
+ android:ellipsize="end"
978
+ android:gravity="center" />
979
+
980
+ </LinearLayout>
981
+ `;
982
+ }
983
+ /**
984
+ * Generate blur background drawable for API 31+ (Android 12+)
985
+ * Uses the system widget background with rounded corners
986
+ */
987
+ function generateBlurBackgroundDrawableV31() {
988
+ return `<?xml version="1.0" encoding="utf-8"?>
989
+ <shape xmlns:android="http://schemas.android.com/apk/res/android"
990
+ android:shape="rectangle">
991
+ <corners android:radius="16dp" />
992
+ <solid android:color="@android:color/system_accent1_50" />
993
+ </shape>
994
+ `;
995
+ }
996
+ /**
997
+ * Generate blur background drawable for pre-API 31 (fallback)
998
+ * Uses semi-transparent white with rounded corners
999
+ */
1000
+ function generateBlurBackgroundDrawableFallback() {
1001
+ return `<?xml version="1.0" encoding="utf-8"?>
1002
+ <shape xmlns:android="http://schemas.android.com/apk/res/android"
1003
+ android:shape="rectangle">
1004
+ <corners android:radius="16dp" />
1005
+ <solid android:color="#E6FFFFFF" />
1006
+ </shape>
1007
+ `;
1008
+ }
539
1009
  function generateWidgetInfoXml(w, updatePeriodMillis) {
540
1010
  return `<?xml version="1.0" encoding="utf-8"?>
541
1011
  <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
@@ -567,17 +1037,29 @@ const withAndroidWidget = (config, props = {}) => {
567
1037
  const resXmlPath = path.join(androidPath, "res", "xml");
568
1038
  const resValuesPath = path.join(androidPath, "res", "values");
569
1039
  const resDrawablePath = path.join(androidPath, "res", "drawable");
1040
+ const resDrawableV31Path = path.join(androidPath, "res", "drawable-v31");
570
1041
  [
571
1042
  widgetPackagePath,
572
1043
  resLayoutPath,
573
1044
  resXmlPath,
574
1045
  resValuesPath,
575
1046
  resDrawablePath,
1047
+ resDrawableV31Path,
576
1048
  ].forEach((dir) => {
577
1049
  if (!fs.existsSync(dir)) {
578
1050
  fs.mkdirSync(dir, { recursive: true });
579
1051
  }
580
1052
  });
1053
+ // Check if any widget uses blur background
1054
+ const hasBlurWidget = resolvedProps.widgets.some((w) => isBlurBackground(w.style?.backgroundColor));
1055
+ // Generate blur background drawables if needed
1056
+ if (hasBlurWidget) {
1057
+ // Fallback drawable (pre-API 31)
1058
+ fs.writeFileSync(path.join(resDrawablePath, "widget_blur_background.xml"), generateBlurBackgroundDrawableFallback());
1059
+ // API 31+ drawable
1060
+ fs.writeFileSync(path.join(resDrawableV31Path, "widget_blur_background.xml"), generateBlurBackgroundDrawableV31());
1061
+ console.log("[nn-widgets] Generated Android blur background drawables");
1062
+ }
581
1063
  // Generate files for each widget
582
1064
  for (const w of resolvedProps.widgets) {
583
1065
  // Widget provider Kotlin
@@ -591,6 +1073,19 @@ const withAndroidWidget = (config, props = {}) => {
591
1073
  const itemLayoutXml = generateWidgetItemLayoutXml(w);
592
1074
  fs.writeFileSync(path.join(resLayoutPath, `${w.name.toLowerCase()}_item.xml`), itemLayoutXml);
593
1075
  }
1076
+ // Flex-grid layouts (row, card cell, list cell)
1077
+ if (w.type === "flex-grid") {
1078
+ // Row layout
1079
+ const rowLayoutXml = generateFlexGridRowLayoutXml(w);
1080
+ fs.writeFileSync(path.join(resLayoutPath, `${w.name.toLowerCase()}_row.xml`), rowLayoutXml);
1081
+ // Card cell layout (icon on top, text below)
1082
+ const cellCardLayoutXml = generateFlexGridCellCardLayoutXml(w);
1083
+ fs.writeFileSync(path.join(resLayoutPath, `${w.name.toLowerCase()}_cell_card.xml`), cellCardLayoutXml);
1084
+ // List cell layout (icon left, text right)
1085
+ const cellListLayoutXml = generateFlexGridCellListLayoutXml(w);
1086
+ fs.writeFileSync(path.join(resLayoutPath, `${w.name.toLowerCase()}_cell_list.xml`), cellListLayoutXml);
1087
+ console.log(`[nn-widgets] Generated Android flex-grid layouts for ${w.name}`);
1088
+ }
594
1089
  // Widget info XML
595
1090
  const widgetInfoXml = generateWidgetInfoXml(w, resolvedProps.android.updatePeriodMillis);
596
1091
  fs.writeFileSync(path.join(resXmlPath, `${w.name.toLowerCase()}_info.xml`), widgetInfoXml);