nn-widgets 0.1.16 → 0.1.17

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.17",
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;AAo8BjB,eAAO,MAAM,iBAAiB,EAAE,YAAY,CAAC,oBAAoB,CAmPhE,CAAC;AAEF,eAAe,iBAAiB,CAAC"}
@@ -286,6 +286,264 @@ class ${w.name}Provider : AppWidgetProvider() {
286
286
  }
287
287
  }
288
288
  }
289
+ `;
290
+ }
291
+ // ── Flex-grid type provider ──
292
+ if (w.type === "flex-grid") {
293
+ const bgColor = w.style?.backgroundColor || "#FFFFFF";
294
+ const titleColor = w.style?.titleColor
295
+ ? `android.graphics.Color.parseColor("${w.style.titleColor}")`
296
+ : "android.graphics.Color.BLACK";
297
+ const subtitleColor = w.style?.subtitleColor
298
+ ? `android.graphics.Color.parseColor("${w.style.subtitleColor}")`
299
+ : "android.graphics.Color.GRAY";
300
+ const accentColor = w.style?.accentColor
301
+ ? `android.graphics.Color.parseColor("${w.style.accentColor}")`
302
+ : 'android.graphics.Color.parseColor("#6200EE")';
303
+ return `package ${packageName}.widget
304
+
305
+ import android.app.PendingIntent
306
+ import android.appwidget.AppWidgetManager
307
+ import android.appwidget.AppWidgetProvider
308
+ import android.content.Context
309
+ import android.content.Intent
310
+ import android.view.View
311
+ import android.widget.RemoteViews
312
+ import ${packageName}.MainActivity
313
+ import ${packageName}.R
314
+ import org.json.JSONArray
315
+ import org.json.JSONObject
316
+
317
+ /**
318
+ * ${w.name} - Flex Grid Widget Provider
319
+ * Auto-generated by nn-widgets
320
+ */
321
+ class ${w.name}Provider : AppWidgetProvider() {
322
+
323
+ companion object {
324
+ const val PREFS_NAME = "nn_widgets_data"
325
+ const val MAX_ROWS = 4
326
+ const val MAX_COLS = 4
327
+ }
328
+
329
+ override fun onUpdate(
330
+ context: Context,
331
+ appWidgetManager: AppWidgetManager,
332
+ appWidgetIds: IntArray
333
+ ) {
334
+ for (appWidgetId in appWidgetIds) {
335
+ updateAppWidget(context, appWidgetManager, appWidgetId)
336
+ }
337
+ }
338
+
339
+ override fun onEnabled(context: Context) {}
340
+ override fun onDisabled(context: Context) {}
341
+
342
+ override fun onReceive(context: Context, intent: Intent) {
343
+ super.onReceive(context, intent)
344
+ if (intent.action == "\${context.packageName}.WIDGET_UPDATE_${w.name.toUpperCase()}") {
345
+ val appWidgetManager = AppWidgetManager.getInstance(context)
346
+ val thisWidget = android.content.ComponentName(context, ${w.name}Provider::class.java)
347
+ val appWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget)
348
+ onUpdate(context, appWidgetManager, appWidgetIds)
349
+ }
350
+ }
351
+
352
+ private fun updateAppWidget(
353
+ context: Context,
354
+ appWidgetManager: AppWidgetManager,
355
+ appWidgetId: Int
356
+ ) {
357
+ val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
358
+ val views = RemoteViews(context.packageName, R.layout.${w.name.toLowerCase()}_layout)
359
+
360
+ // Read items and layout JSON (same API as iOS)
361
+ val itemsJson = prefs.getString("widget_${w.name}_items", null)
362
+ val layoutJson = prefs.getString("widget_${w.name}_layout", null)
363
+ val title = prefs.getString("widget_${w.name}_title", "${w.fallbackTitle}") ?: "${w.fallbackTitle}"
364
+ val subtitle = prefs.getString("widget_${w.name}_subtitle", "${w.fallbackSubtitle}") ?: "${w.fallbackSubtitle}"
365
+
366
+ if (itemsJson != null && layoutJson != null) {
367
+ try {
368
+ val items = JSONArray(itemsJson)
369
+ val layoutObj = JSONObject(layoutJson)
370
+ // Get layout for systemSmall (default) - same as iOS
371
+ val layout = layoutObj.optJSONArray("systemSmall") ?: JSONArray("[[1]]")
372
+
373
+ // Hide fallback, show grid
374
+ views.setViewVisibility(R.id.widget_fallback, View.GONE)
375
+ views.setViewVisibility(R.id.widget_grid_container, View.VISIBLE)
376
+
377
+ // Clear existing rows
378
+ views.removeAllViews(R.id.widget_grid_container)
379
+
380
+ var itemIndex = 0
381
+ val rowCount = minOf(layout.length(), MAX_ROWS)
382
+
383
+ for (rowIdx in 0 until rowCount) {
384
+ val rowRatios = layout.optJSONArray(rowIdx) ?: continue
385
+ val colCount = minOf(rowRatios.length(), MAX_COLS)
386
+
387
+ // Create row container
388
+ val rowView = RemoteViews(context.packageName, R.layout.${w.name.toLowerCase()}_row)
389
+
390
+ for (colIdx in 0 until colCount) {
391
+ val ratio = rowRatios.optDouble(colIdx, 1.0)
392
+ val isListStyle = ratio >= 1.0
393
+
394
+ if (itemIndex < items.length()) {
395
+ val item = items.getJSONObject(itemIndex)
396
+ val cellView = if (isListStyle) {
397
+ createListStyleCell(context, item, itemIndex)
398
+ } else {
399
+ createCardStyleCell(context, item, itemIndex)
400
+ }
401
+ rowView.addView(R.id.row_container, cellView)
402
+ }
403
+ itemIndex++
404
+ }
405
+
406
+ views.addView(R.id.widget_grid_container, rowView)
407
+ }
408
+ } catch (e: Exception) {
409
+ android.util.Log.e("${w.name}Provider", "Error parsing widget data", e)
410
+ showFallback(views, title, subtitle)
411
+ }
412
+ } else {
413
+ showFallback(views, title, subtitle)
414
+ }
415
+
416
+ // Widget-level click
417
+ ${intentCode}
418
+ val pendingIntent = PendingIntent.getActivity(
419
+ context, 100, intent,
420
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
421
+ )
422
+ views.setOnClickPendingIntent(R.id.widget_container, pendingIntent)
423
+
424
+ appWidgetManager.updateAppWidget(appWidgetId, views)
425
+ }
426
+
427
+ private fun createListStyleCell(context: Context, item: JSONObject, index: Int): RemoteViews {
428
+ val cellView = RemoteViews(context.packageName, R.layout.${w.name.toLowerCase()}_cell_list)
429
+
430
+ // Parse title
431
+ val itemTitle = parseTextValue(item, "title")
432
+ cellView.setTextViewText(R.id.cell_title, itemTitle)
433
+ cellView.setTextColor(R.id.cell_title, ${titleColor})
434
+
435
+ // Parse description
436
+ val itemDesc = parseTextValue(item, "description")
437
+ if (itemDesc.isNotEmpty()) {
438
+ cellView.setTextViewText(R.id.cell_description, itemDesc)
439
+ cellView.setTextColor(R.id.cell_description, ${subtitleColor})
440
+ cellView.setViewVisibility(R.id.cell_description, View.VISIBLE)
441
+ } else {
442
+ cellView.setViewVisibility(R.id.cell_description, View.GONE)
443
+ }
444
+
445
+ // Parse icon
446
+ setupIcon(context, cellView, item, R.id.cell_icon)
447
+
448
+ // Deep link
449
+ setupDeepLink(context, cellView, item, index, R.id.cell_container)
450
+
451
+ return cellView
452
+ }
453
+
454
+ private fun createCardStyleCell(context: Context, item: JSONObject, index: Int): RemoteViews {
455
+ val cellView = RemoteViews(context.packageName, R.layout.${w.name.toLowerCase()}_cell_card)
456
+
457
+ // Parse title (smaller for card style)
458
+ val itemTitle = parseTextValue(item, "title")
459
+ cellView.setTextViewText(R.id.cell_title, itemTitle)
460
+ cellView.setTextColor(R.id.cell_title, ${titleColor})
461
+
462
+ // Parse icon (larger for card style)
463
+ setupIcon(context, cellView, item, R.id.cell_icon)
464
+
465
+ // Deep link
466
+ setupDeepLink(context, cellView, item, index, R.id.cell_container)
467
+
468
+ return cellView
469
+ }
470
+
471
+ private fun setupIcon(context: Context, views: RemoteViews, item: JSONObject, iconViewId: Int) {
472
+ val iconName = parseIconName(item)
473
+ if (iconName.isNotEmpty()) {
474
+ // Try to load from drawable resources
475
+ val resId = context.resources.getIdentifier(
476
+ iconName.replace("-", "_").lowercase(),
477
+ "drawable",
478
+ context.packageName
479
+ )
480
+ if (resId != 0) {
481
+ views.setImageViewResource(iconViewId, resId)
482
+ views.setViewVisibility(iconViewId, View.VISIBLE)
483
+ } else {
484
+ // Fallback to a default icon or hide
485
+ views.setViewVisibility(iconViewId, View.GONE)
486
+ }
487
+ } else {
488
+ views.setViewVisibility(iconViewId, View.GONE)
489
+ }
490
+ }
491
+
492
+ private fun setupDeepLink(context: Context, views: RemoteViews, item: JSONObject, index: Int, containerId: Int) {
493
+ val deepLink = item.optString("deepLink", "")
494
+ if (deepLink.isNotEmpty()) {
495
+ val itemIntent = Intent(Intent.ACTION_VIEW, android.net.Uri.parse(deepLink)).apply {
496
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
497
+ }
498
+ val pendingIntent = PendingIntent.getActivity(
499
+ context, index, itemIntent,
500
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
501
+ )
502
+ views.setOnClickPendingIntent(containerId, pendingIntent)
503
+ }
504
+ }
505
+
506
+ private fun showFallback(views: RemoteViews, title: String, subtitle: String) {
507
+ views.setViewVisibility(R.id.widget_fallback, View.VISIBLE)
508
+ views.setViewVisibility(R.id.widget_grid_container, View.GONE)
509
+ views.setTextViewText(R.id.fallback_title, title)
510
+ views.setTextColor(R.id.fallback_title, ${titleColor})
511
+ views.setTextViewText(R.id.fallback_subtitle, subtitle)
512
+ views.setTextColor(R.id.fallback_subtitle, ${subtitleColor})
513
+ }
514
+
515
+ private fun parseTextValue(item: JSONObject, key: String): String {
516
+ return when {
517
+ item.has(key) && item.get(key) is String -> item.getString(key)
518
+ item.has(key) && item.get(key) is JSONObject -> item.getJSONObject(key).optString("text", "")
519
+ else -> ""
520
+ }
521
+ }
522
+
523
+ private fun parseIconName(item: JSONObject): String {
524
+ if (!item.has("icon")) return ""
525
+ return when {
526
+ item.get("icon") is String -> {
527
+ val iconStr = item.getString("icon")
528
+ // Extract filename from URL if it's a URL
529
+ if (iconStr.startsWith("http")) {
530
+ iconStr.substringAfterLast("/").substringBeforeLast(".")
531
+ } else {
532
+ iconStr
533
+ }
534
+ }
535
+ item.get("icon") is JSONObject -> {
536
+ val iconUrl = item.getJSONObject("icon").optString("url", "")
537
+ if (iconUrl.startsWith("http")) {
538
+ iconUrl.substringAfterLast("/").substringBeforeLast(".")
539
+ } else {
540
+ iconUrl
541
+ }
542
+ }
543
+ else -> ""
544
+ }
545
+ }
546
+ }
289
547
  `;
290
548
  }
291
549
  // ── Single-type provider (default) ──
@@ -436,6 +694,59 @@ function generateWidgetLayoutXml(w) {
436
694
  android:orientation="vertical"
437
695
  android:visibility="gone" />
438
696
 
697
+ </LinearLayout>
698
+ `;
699
+ }
700
+ // ── Flex-grid layout ──
701
+ if (w.type === "flex-grid") {
702
+ const bgColor = w.style?.backgroundColor || "#FFFFFF";
703
+ const titleColor = w.style?.titleColor || "#000000";
704
+ const subtitleColor = w.style?.subtitleColor || "#888888";
705
+ return `<?xml version="1.0" encoding="utf-8"?>
706
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
707
+ android:id="@+id/widget_container"
708
+ android:layout_width="match_parent"
709
+ android:layout_height="match_parent"
710
+ android:orientation="vertical"
711
+ android:padding="8dp"
712
+ android:background="${bgColor}">
713
+
714
+ <!-- Fallback view (shown when no data) -->
715
+ <LinearLayout
716
+ android:id="@+id/widget_fallback"
717
+ android:layout_width="match_parent"
718
+ android:layout_height="match_parent"
719
+ android:orientation="vertical"
720
+ android:gravity="center"
721
+ android:visibility="visible">
722
+
723
+ <TextView
724
+ android:id="@+id/fallback_title"
725
+ android:layout_width="wrap_content"
726
+ android:layout_height="wrap_content"
727
+ android:text="${w.fallbackTitle}"
728
+ android:textSize="16sp"
729
+ android:textStyle="bold"
730
+ android:textColor="${titleColor}" />
731
+
732
+ <TextView
733
+ android:id="@+id/fallback_subtitle"
734
+ android:layout_width="wrap_content"
735
+ android:layout_height="wrap_content"
736
+ android:layout_marginTop="4dp"
737
+ android:text="${w.fallbackSubtitle}"
738
+ android:textSize="12sp"
739
+ android:textColor="${subtitleColor}" />
740
+ </LinearLayout>
741
+
742
+ <!-- Grid container (populated dynamically with rows) -->
743
+ <LinearLayout
744
+ android:id="@+id/widget_grid_container"
745
+ android:layout_width="match_parent"
746
+ android:layout_height="match_parent"
747
+ android:orientation="vertical"
748
+ android:visibility="gone" />
749
+
439
750
  </LinearLayout>
440
751
  `;
441
752
  }
@@ -533,6 +844,107 @@ function generateWidgetItemLayoutXml(w) {
533
844
  android:visibility="gone" />
534
845
  </LinearLayout>
535
846
 
847
+ </LinearLayout>
848
+ `;
849
+ }
850
+ /** Generate row layout XML for flex-grid widgets */
851
+ function generateFlexGridRowLayoutXml(w) {
852
+ return `<?xml version="1.0" encoding="utf-8"?>
853
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
854
+ android:id="@+id/row_container"
855
+ android:layout_width="match_parent"
856
+ android:layout_height="0dp"
857
+ android:layout_weight="1"
858
+ android:orientation="horizontal"
859
+ android:gravity="center" />
860
+ `;
861
+ }
862
+ /** Generate list-style cell layout XML for flex-grid widgets */
863
+ function generateFlexGridCellListLayoutXml(w) {
864
+ const titleColor = w.style?.titleColor || "#000000";
865
+ const subtitleColor = w.style?.subtitleColor || "#888888";
866
+ const accentColor = w.style?.accentColor || "#6200EE";
867
+ return `<?xml version="1.0" encoding="utf-8"?>
868
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
869
+ android:id="@+id/cell_container"
870
+ android:layout_width="0dp"
871
+ android:layout_height="match_parent"
872
+ android:layout_weight="1"
873
+ android:orientation="horizontal"
874
+ android:gravity="center_vertical"
875
+ android:paddingHorizontal="6dp"
876
+ android:paddingVertical="4dp">
877
+
878
+ <ImageView
879
+ android:id="@+id/cell_icon"
880
+ android:layout_width="36dp"
881
+ android:layout_height="36dp"
882
+ android:layout_marginEnd="8dp"
883
+ android:scaleType="centerCrop"
884
+ android:visibility="gone" />
885
+
886
+ <LinearLayout
887
+ android:layout_width="0dp"
888
+ android:layout_height="wrap_content"
889
+ android:layout_weight="1"
890
+ android:orientation="vertical">
891
+
892
+ <TextView
893
+ android:id="@+id/cell_title"
894
+ android:layout_width="match_parent"
895
+ android:layout_height="wrap_content"
896
+ android:textSize="13sp"
897
+ android:textStyle="bold"
898
+ android:textColor="${titleColor}"
899
+ android:maxLines="1"
900
+ android:ellipsize="end" />
901
+
902
+ <TextView
903
+ android:id="@+id/cell_description"
904
+ android:layout_width="match_parent"
905
+ android:layout_height="wrap_content"
906
+ android:textSize="11sp"
907
+ android:textColor="${subtitleColor}"
908
+ android:maxLines="1"
909
+ android:ellipsize="end"
910
+ android:visibility="gone" />
911
+ </LinearLayout>
912
+
913
+ </LinearLayout>
914
+ `;
915
+ }
916
+ /** Generate card-style cell layout XML for flex-grid widgets */
917
+ function generateFlexGridCellCardLayoutXml(w) {
918
+ const titleColor = w.style?.titleColor || "#000000";
919
+ const accentColor = w.style?.accentColor || "#6200EE";
920
+ return `<?xml version="1.0" encoding="utf-8"?>
921
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
922
+ android:id="@+id/cell_container"
923
+ android:layout_width="0dp"
924
+ android:layout_height="match_parent"
925
+ android:layout_weight="1"
926
+ android:orientation="vertical"
927
+ android:gravity="center"
928
+ android:padding="4dp">
929
+
930
+ <ImageView
931
+ android:id="@+id/cell_icon"
932
+ android:layout_width="40dp"
933
+ android:layout_height="40dp"
934
+ android:scaleType="centerCrop"
935
+ android:visibility="gone" />
936
+
937
+ <TextView
938
+ android:id="@+id/cell_title"
939
+ android:layout_width="match_parent"
940
+ android:layout_height="wrap_content"
941
+ android:layout_marginTop="4dp"
942
+ android:textSize="10sp"
943
+ android:textColor="${titleColor}"
944
+ android:maxLines="2"
945
+ android:ellipsize="end"
946
+ android:gravity="center" />
947
+
536
948
  </LinearLayout>
537
949
  `;
538
950
  }
@@ -591,6 +1003,19 @@ const withAndroidWidget = (config, props = {}) => {
591
1003
  const itemLayoutXml = generateWidgetItemLayoutXml(w);
592
1004
  fs.writeFileSync(path.join(resLayoutPath, `${w.name.toLowerCase()}_item.xml`), itemLayoutXml);
593
1005
  }
1006
+ // Flex-grid layouts (row, card cell, list cell)
1007
+ if (w.type === "flex-grid") {
1008
+ // Row layout
1009
+ const rowLayoutXml = generateFlexGridRowLayoutXml(w);
1010
+ fs.writeFileSync(path.join(resLayoutPath, `${w.name.toLowerCase()}_row.xml`), rowLayoutXml);
1011
+ // Card cell layout (icon on top, text below)
1012
+ const cellCardLayoutXml = generateFlexGridCellCardLayoutXml(w);
1013
+ fs.writeFileSync(path.join(resLayoutPath, `${w.name.toLowerCase()}_cell_card.xml`), cellCardLayoutXml);
1014
+ // List cell layout (icon left, text right)
1015
+ const cellListLayoutXml = generateFlexGridCellListLayoutXml(w);
1016
+ fs.writeFileSync(path.join(resLayoutPath, `${w.name.toLowerCase()}_cell_list.xml`), cellListLayoutXml);
1017
+ console.log(`[nn-widgets] Generated Android flex-grid layouts for ${w.name}`);
1018
+ }
594
1019
  // Widget info XML
595
1020
  const widgetInfoXml = generateWidgetInfoXml(w, resolvedProps.android.updatePeriodMillis);
596
1021
  fs.writeFileSync(path.join(resXmlPath, `${w.name.toLowerCase()}_info.xml`), widgetInfoXml);