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 +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;
|
|
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
|
|
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
|
|
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);
|