nn-widgets 0.1.0

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.
@@ -0,0 +1,700 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.withAndroidWidget = void 0;
37
+ const config_plugins_1 = require("@expo/config-plugins");
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const withIosWidget_1 = require("./withIosWidget");
41
+ // ──────────────────────────────────────────────
42
+ // Code generation helpers (per widget)
43
+ // ──────────────────────────────────────────────
44
+ function generateWidgetProviderCode(w, packageName, deepLinkUrl) {
45
+ const hasImage = w.image && (w.image.small || w.image.medium || w.image.large);
46
+ const intentCode = deepLinkUrl
47
+ ? `val intent = Intent(Intent.ACTION_VIEW, android.net.Uri.parse("${deepLinkUrl}")).apply {
48
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
49
+ }`
50
+ : `val intent = Intent(context, MainActivity::class.java).apply {
51
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
52
+ }`;
53
+ // ── Image-only provider ──
54
+ if (w.type === "image" && hasImage) {
55
+ return `package ${packageName}.widget
56
+
57
+ import android.app.PendingIntent
58
+ import android.appwidget.AppWidgetManager
59
+ import android.appwidget.AppWidgetProvider
60
+ import android.content.Context
61
+ import android.content.Intent
62
+ import android.widget.RemoteViews
63
+ import ${packageName}.MainActivity
64
+
65
+ /**
66
+ * ${w.name} - Image Widget Provider
67
+ * Auto-generated by nn-widgets
68
+ */
69
+ class ${w.name}Provider : AppWidgetProvider() {
70
+
71
+ override fun onUpdate(
72
+ context: Context,
73
+ appWidgetManager: AppWidgetManager,
74
+ appWidgetIds: IntArray
75
+ ) {
76
+ for (appWidgetId in appWidgetIds) {
77
+ updateAppWidget(context, appWidgetManager, appWidgetId)
78
+ }
79
+ }
80
+
81
+ override fun onEnabled(context: Context) {}
82
+ override fun onDisabled(context: Context) {}
83
+
84
+ override fun onReceive(context: Context, intent: Intent) {
85
+ super.onReceive(context, intent)
86
+ if (intent.action == "\${context.packageName}.WIDGET_UPDATE_${w.name.toUpperCase()}") {
87
+ val appWidgetManager = AppWidgetManager.getInstance(context)
88
+ val thisWidget = android.content.ComponentName(context, ${w.name}Provider::class.java)
89
+ val appWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget)
90
+ onUpdate(context, appWidgetManager, appWidgetIds)
91
+ }
92
+ }
93
+
94
+ private fun updateAppWidget(
95
+ context: Context,
96
+ appWidgetManager: AppWidgetManager,
97
+ appWidgetId: Int
98
+ ) {
99
+ val views = RemoteViews(context.packageName, R.layout.${w.name.toLowerCase()}_layout)
100
+
101
+ ${intentCode}
102
+ val pendingIntent = PendingIntent.getActivity(
103
+ context,
104
+ 0,
105
+ intent,
106
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
107
+ )
108
+ views.setOnClickPendingIntent(R.id.widget_container, pendingIntent)
109
+
110
+ appWidgetManager.updateAppWidget(appWidgetId, views)
111
+ }
112
+ }
113
+ `;
114
+ }
115
+ // ── List-type provider ──
116
+ if (w.type === "list") {
117
+ const bgColor = w.style?.backgroundColor || "#FFFFFF";
118
+ const titleColor = w.style?.titleColor
119
+ ? `android.graphics.Color.parseColor("${w.style.titleColor}")`
120
+ : "android.graphics.Color.BLACK";
121
+ const subtitleColor = w.style?.subtitleColor
122
+ ? `android.graphics.Color.parseColor("${w.style.subtitleColor}")`
123
+ : "android.graphics.Color.GRAY";
124
+ const accentColor = w.style?.accentColor
125
+ ? `android.graphics.Color.parseColor("${w.style.accentColor}")`
126
+ : 'android.graphics.Color.parseColor("#6200EE")';
127
+ return `package ${packageName}.widget
128
+
129
+ import android.app.PendingIntent
130
+ import android.appwidget.AppWidgetManager
131
+ import android.appwidget.AppWidgetProvider
132
+ import android.content.Context
133
+ import android.content.Intent
134
+ import android.graphics.BitmapFactory
135
+ import android.view.View
136
+ import android.widget.RemoteViews
137
+ import ${packageName}.MainActivity
138
+ import ${packageName}.R
139
+ import org.json.JSONArray
140
+ import org.json.JSONObject
141
+
142
+ /**
143
+ * ${w.name} - List Widget Provider
144
+ * Auto-generated by nn-widgets
145
+ */
146
+ class ${w.name}Provider : AppWidgetProvider() {
147
+
148
+ companion object {
149
+ const val PREFS_NAME = "nn_widgets_data"
150
+ const val MAX_ITEMS = 5
151
+ }
152
+
153
+ override fun onUpdate(
154
+ context: Context,
155
+ appWidgetManager: AppWidgetManager,
156
+ appWidgetIds: IntArray
157
+ ) {
158
+ for (appWidgetId in appWidgetIds) {
159
+ updateAppWidget(context, appWidgetManager, appWidgetId)
160
+ }
161
+ }
162
+
163
+ override fun onEnabled(context: Context) {}
164
+ override fun onDisabled(context: Context) {}
165
+
166
+ override fun onReceive(context: Context, intent: Intent) {
167
+ super.onReceive(context, intent)
168
+ if (intent.action == "\${context.packageName}.WIDGET_UPDATE_${w.name.toUpperCase()}") {
169
+ val appWidgetManager = AppWidgetManager.getInstance(context)
170
+ val thisWidget = android.content.ComponentName(context, ${w.name}Provider::class.java)
171
+ val appWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget)
172
+ onUpdate(context, appWidgetManager, appWidgetIds)
173
+ }
174
+ }
175
+
176
+ private fun updateAppWidget(
177
+ context: Context,
178
+ appWidgetManager: AppWidgetManager,
179
+ appWidgetId: Int
180
+ ) {
181
+ val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
182
+ val views = RemoteViews(context.packageName, R.layout.${w.name.toLowerCase()}_layout)
183
+
184
+ // Read items JSON
185
+ val itemsJson = prefs.getString("widget_${w.name}_items", null)
186
+ val title = prefs.getString("widget_${w.name}_title", "${w.fallbackTitle}") ?: "${w.fallbackTitle}"
187
+ val subtitle = prefs.getString("widget_${w.name}_subtitle", "${w.fallbackSubtitle}") ?: "${w.fallbackSubtitle}"
188
+
189
+ if (itemsJson != null) {
190
+ try {
191
+ val items = JSONArray(itemsJson)
192
+ val count = minOf(items.length(), MAX_ITEMS)
193
+
194
+ // Show list container, hide fallback
195
+ views.setViewVisibility(R.id.widget_fallback, View.GONE)
196
+ views.setViewVisibility(R.id.widget_list_container, View.VISIBLE)
197
+
198
+ // Remove all existing items
199
+ views.removeAllViews(R.id.widget_list_container)
200
+
201
+ for (i in 0 until count) {
202
+ val item = items.getJSONObject(i)
203
+ val itemView = RemoteViews(context.packageName, R.layout.${w.name.toLowerCase()}_item)
204
+
205
+ // Parse title
206
+ val itemTitle = parseTextValue(item, "title")
207
+ itemView.setTextViewText(R.id.item_title, itemTitle)
208
+
209
+ // Parse description
210
+ val itemDesc = parseTextValue(item, "description")
211
+ if (itemDesc.isNotEmpty()) {
212
+ itemView.setTextViewText(R.id.item_description, itemDesc)
213
+ itemView.setViewVisibility(R.id.item_description, View.VISIBLE)
214
+ } else {
215
+ itemView.setViewVisibility(R.id.item_description, View.GONE)
216
+ }
217
+
218
+ // Parse icon
219
+ val iconName = parseIconName(item)
220
+ if (iconName.isNotEmpty()) {
221
+ val resId = context.resources.getIdentifier(iconName, "drawable", context.packageName)
222
+ if (resId != 0) {
223
+ itemView.setImageViewResource(R.id.item_icon, resId)
224
+ itemView.setViewVisibility(R.id.item_icon, View.VISIBLE)
225
+ } else {
226
+ itemView.setViewVisibility(R.id.item_icon, View.GONE)
227
+ }
228
+ } else {
229
+ itemView.setViewVisibility(R.id.item_icon, View.GONE)
230
+ }
231
+
232
+ // Per-item deep link
233
+ val deepLink = item.optString("deepLink", "")
234
+ if (deepLink.isNotEmpty()) {
235
+ val itemIntent = Intent(Intent.ACTION_VIEW, android.net.Uri.parse(deepLink)).apply {
236
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
237
+ }
238
+ val pendingIntent = PendingIntent.getActivity(
239
+ context, i, itemIntent,
240
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
241
+ )
242
+ itemView.setOnClickPendingIntent(R.id.item_container, pendingIntent)
243
+ }
244
+
245
+ views.addView(R.id.widget_list_container, itemView)
246
+ }
247
+ } catch (e: Exception) {
248
+ showFallback(views, title, subtitle)
249
+ }
250
+ } else {
251
+ showFallback(views, title, subtitle)
252
+ }
253
+
254
+ // Widget-level click
255
+ ${intentCode}
256
+ val pendingIntent = PendingIntent.getActivity(
257
+ context, 100, intent,
258
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
259
+ )
260
+ views.setOnClickPendingIntent(R.id.widget_container, pendingIntent)
261
+
262
+ appWidgetManager.updateAppWidget(appWidgetId, views)
263
+ }
264
+
265
+ private fun showFallback(views: RemoteViews, title: String, subtitle: String) {
266
+ views.setViewVisibility(R.id.widget_fallback, View.VISIBLE)
267
+ views.setViewVisibility(R.id.widget_list_container, View.GONE)
268
+ views.setTextViewText(R.id.fallback_title, title)
269
+ views.setTextViewText(R.id.fallback_subtitle, subtitle)
270
+ }
271
+
272
+ private fun parseTextValue(item: JSONObject, key: String): String {
273
+ return when {
274
+ item.has(key) && item.get(key) is String -> item.getString(key)
275
+ item.has(key) && item.get(key) is JSONObject -> item.getJSONObject(key).optString("text", "")
276
+ else -> ""
277
+ }
278
+ }
279
+
280
+ private fun parseIconName(item: JSONObject): String {
281
+ if (!item.has("icon")) return ""
282
+ return when {
283
+ item.get("icon") is String -> item.getString("icon")
284
+ item.get("icon") is JSONObject -> item.getJSONObject("icon").optString("url", "")
285
+ else -> ""
286
+ }
287
+ }
288
+ }
289
+ `;
290
+ }
291
+ // ── Single-type provider (default) ──
292
+ return `package ${packageName}.widget
293
+
294
+ import android.app.PendingIntent
295
+ import android.appwidget.AppWidgetManager
296
+ import android.appwidget.AppWidgetProvider
297
+ import android.content.Context
298
+ import android.content.Intent
299
+ import android.widget.RemoteViews
300
+ import ${packageName}.MainActivity
301
+
302
+ /**
303
+ * ${w.name} - App Widget Provider
304
+ * Auto-generated by nn-widgets
305
+ */
306
+ class ${w.name}Provider : AppWidgetProvider() {
307
+
308
+ companion object {
309
+ const val PREFS_NAME = "nn_widgets_data"
310
+ }
311
+
312
+ override fun onUpdate(
313
+ context: Context,
314
+ appWidgetManager: AppWidgetManager,
315
+ appWidgetIds: IntArray
316
+ ) {
317
+ for (appWidgetId in appWidgetIds) {
318
+ updateAppWidget(context, appWidgetManager, appWidgetId)
319
+ }
320
+ }
321
+
322
+ override fun onEnabled(context: Context) {}
323
+
324
+ override fun onDisabled(context: Context) {}
325
+
326
+ override fun onReceive(context: Context, intent: Intent) {
327
+ super.onReceive(context, intent)
328
+ if (intent.action == "\${context.packageName}.WIDGET_UPDATE_${w.name.toUpperCase()}") {
329
+ val appWidgetManager = AppWidgetManager.getInstance(context)
330
+ val thisWidget = android.content.ComponentName(context, ${w.name}Provider::class.java)
331
+ val appWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget)
332
+ onUpdate(context, appWidgetManager, appWidgetIds)
333
+ }
334
+ }
335
+
336
+ private fun updateAppWidget(
337
+ context: Context,
338
+ appWidgetManager: AppWidgetManager,
339
+ appWidgetId: Int
340
+ ) {
341
+ val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
342
+
343
+ val title = prefs.getString("widget_${w.name}_title", "${w.displayName}") ?: "${w.displayName}"
344
+ val subtitle = prefs.getString("widget_${w.name}_subtitle", "") ?: ""
345
+ val value = prefs.getInt("widget_${w.name}_value", 0)
346
+
347
+ val views = RemoteViews(context.packageName, R.layout.${w.name.toLowerCase()}_layout)
348
+
349
+ views.setTextViewText(R.id.widget_title, title)
350
+ views.setTextViewText(R.id.widget_subtitle, subtitle)
351
+ views.setTextViewText(R.id.widget_value, value.toString())
352
+
353
+ // Create intent when widget is clicked
354
+ ${intentCode}
355
+ val pendingIntent = PendingIntent.getActivity(
356
+ context,
357
+ 0,
358
+ intent,
359
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
360
+ )
361
+ views.setOnClickPendingIntent(R.id.widget_container, pendingIntent)
362
+
363
+ appWidgetManager.updateAppWidget(appWidgetId, views)
364
+ }
365
+ }
366
+ `;
367
+ }
368
+ function generateWidgetLayoutXml(w) {
369
+ const hasImage = w.image && (w.image.small || w.image.medium || w.image.large);
370
+ // ── Image layout ──
371
+ if (w.type === "image" && hasImage) {
372
+ const drawableName = `${w.name.toLowerCase()}_bg`;
373
+ return `<?xml version="1.0" encoding="utf-8"?>
374
+ <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
375
+ android:id="@+id/widget_container"
376
+ android:layout_width="match_parent"
377
+ android:layout_height="match_parent">
378
+
379
+ <ImageView
380
+ android:id="@+id/widget_image"
381
+ android:layout_width="match_parent"
382
+ android:layout_height="match_parent"
383
+ android:scaleType="centerCrop"
384
+ android:src="@drawable/${drawableName}" />
385
+
386
+ </FrameLayout>
387
+ `;
388
+ }
389
+ // ── List layout ──
390
+ if (w.type === "list") {
391
+ const bgColor = w.style?.backgroundColor || "#FFFFFF";
392
+ const titleColor = w.style?.titleColor || "#000000";
393
+ const subtitleColor = w.style?.subtitleColor || "#888888";
394
+ return `<?xml version="1.0" encoding="utf-8"?>
395
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
396
+ android:id="@+id/widget_container"
397
+ android:layout_width="match_parent"
398
+ android:layout_height="match_parent"
399
+ android:orientation="vertical"
400
+ android:padding="12dp"
401
+ android:background="${bgColor}">
402
+
403
+ <!-- Fallback view (shown when no data) -->
404
+ <LinearLayout
405
+ android:id="@+id/widget_fallback"
406
+ android:layout_width="match_parent"
407
+ android:layout_height="match_parent"
408
+ android:orientation="vertical"
409
+ android:gravity="center"
410
+ android:visibility="visible">
411
+
412
+ <TextView
413
+ android:id="@+id/fallback_title"
414
+ android:layout_width="wrap_content"
415
+ android:layout_height="wrap_content"
416
+ android:text="${w.fallbackTitle}"
417
+ android:textSize="16sp"
418
+ android:textStyle="bold"
419
+ android:textColor="${titleColor}" />
420
+
421
+ <TextView
422
+ android:id="@+id/fallback_subtitle"
423
+ android:layout_width="wrap_content"
424
+ android:layout_height="wrap_content"
425
+ android:layout_marginTop="4dp"
426
+ android:text="${w.fallbackSubtitle}"
427
+ android:textSize="12sp"
428
+ android:textColor="${subtitleColor}" />
429
+ </LinearLayout>
430
+
431
+ <!-- List container (populated dynamically) -->
432
+ <LinearLayout
433
+ android:id="@+id/widget_list_container"
434
+ android:layout_width="match_parent"
435
+ android:layout_height="match_parent"
436
+ android:orientation="vertical"
437
+ android:visibility="gone" />
438
+
439
+ </LinearLayout>
440
+ `;
441
+ }
442
+ // ── Single (default) styled text layout ──
443
+ const bgColor = w.style?.backgroundColor || "#FFFFFF";
444
+ const titleColor = w.style?.titleColor || "#000000";
445
+ const subtitleColor = w.style?.subtitleColor || "#888888";
446
+ const accentColor = w.style?.accentColor || "#6200EE";
447
+ return `<?xml version="1.0" encoding="utf-8"?>
448
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
449
+ android:id="@+id/widget_container"
450
+ android:layout_width="match_parent"
451
+ android:layout_height="match_parent"
452
+ android:orientation="vertical"
453
+ android:padding="16dp"
454
+ android:background="${bgColor}">
455
+
456
+ <TextView
457
+ android:id="@+id/widget_title"
458
+ android:layout_width="match_parent"
459
+ android:layout_height="wrap_content"
460
+ android:text="${w.displayName}"
461
+ android:textSize="16sp"
462
+ android:textStyle="bold"
463
+ android:textColor="${titleColor}" />
464
+
465
+ <TextView
466
+ android:id="@+id/widget_subtitle"
467
+ android:layout_width="match_parent"
468
+ android:layout_height="wrap_content"
469
+ android:layout_marginTop="4dp"
470
+ android:text=""
471
+ android:textSize="12sp"
472
+ android:textColor="${subtitleColor}" />
473
+
474
+ <TextView
475
+ android:id="@+id/widget_value"
476
+ android:layout_width="match_parent"
477
+ android:layout_height="0dp"
478
+ android:layout_weight="1"
479
+ android:gravity="center"
480
+ android:text="0"
481
+ android:textSize="32sp"
482
+ android:textStyle="bold"
483
+ android:textColor="${accentColor}" />
484
+
485
+ </LinearLayout>
486
+ `;
487
+ }
488
+ /** Generate list item layout XML for list-type widgets */
489
+ function generateWidgetItemLayoutXml(w) {
490
+ const titleColor = w.style?.titleColor || "#000000";
491
+ const subtitleColor = w.style?.subtitleColor || "#888888";
492
+ return `<?xml version="1.0" encoding="utf-8"?>
493
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
494
+ android:id="@+id/item_container"
495
+ android:layout_width="match_parent"
496
+ android:layout_height="wrap_content"
497
+ android:orientation="horizontal"
498
+ android:gravity="center_vertical"
499
+ android:paddingVertical="6dp">
500
+
501
+ <ImageView
502
+ android:id="@+id/item_icon"
503
+ android:layout_width="32dp"
504
+ android:layout_height="32dp"
505
+ android:layout_marginEnd="12dp"
506
+ android:scaleType="centerCrop"
507
+ android:visibility="gone" />
508
+
509
+ <LinearLayout
510
+ android:layout_width="0dp"
511
+ android:layout_height="wrap_content"
512
+ android:layout_weight="1"
513
+ android:orientation="vertical">
514
+
515
+ <TextView
516
+ android:id="@+id/item_title"
517
+ android:layout_width="match_parent"
518
+ android:layout_height="wrap_content"
519
+ android:textSize="14sp"
520
+ android:textStyle="bold"
521
+ android:textColor="${titleColor}"
522
+ android:maxLines="1"
523
+ android:ellipsize="end" />
524
+
525
+ <TextView
526
+ android:id="@+id/item_description"
527
+ android:layout_width="match_parent"
528
+ android:layout_height="wrap_content"
529
+ android:textSize="12sp"
530
+ android:textColor="${subtitleColor}"
531
+ android:maxLines="1"
532
+ android:ellipsize="end"
533
+ android:visibility="gone" />
534
+ </LinearLayout>
535
+
536
+ </LinearLayout>
537
+ `;
538
+ }
539
+ function generateWidgetInfoXml(w, updatePeriodMillis) {
540
+ return `<?xml version="1.0" encoding="utf-8"?>
541
+ <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
542
+ android:minWidth="${w.android.minWidth}dp"
543
+ android:minHeight="${w.android.minHeight}dp"
544
+ android:updatePeriodMillis="${updatePeriodMillis}"
545
+ android:initialLayout="@layout/${w.name.toLowerCase()}_layout"
546
+ android:resizeMode="${w.android.resizeMode}"
547
+ android:widgetCategory="home_screen"
548
+ android:description="@string/${w.name.toLowerCase()}_description"
549
+ android:previewLayout="@layout/${w.name.toLowerCase()}_layout" />
550
+ `;
551
+ }
552
+ // ──────────────────────────────────────────────
553
+ // Config plugin
554
+ // ──────────────────────────────────────────────
555
+ const withAndroidWidget = (config, props = {}) => {
556
+ const packageName = config.android?.package || "com.app.widget";
557
+ const bundleIdentifier = config.ios?.bundleIdentifier || "com.app.widget";
558
+ // Resolve using the shared resolver
559
+ const resolvedProps = (0, withIosWidget_1.resolveIosProps)(props, config.name || "Widget", bundleIdentifier);
560
+ // Modify AndroidManifest + write files for each widget
561
+ config = (0, config_plugins_1.withAndroidManifest)(config, async (config) => {
562
+ const manifest = config.modResults;
563
+ const projectRoot = config.modRequest.projectRoot;
564
+ const androidPath = path.join(projectRoot, "android", "app", "src", "main");
565
+ const widgetPackagePath = path.join(androidPath, "java", ...packageName.split("."), "widget");
566
+ const resLayoutPath = path.join(androidPath, "res", "layout");
567
+ const resXmlPath = path.join(androidPath, "res", "xml");
568
+ const resValuesPath = path.join(androidPath, "res", "values");
569
+ const resDrawablePath = path.join(androidPath, "res", "drawable");
570
+ [
571
+ widgetPackagePath,
572
+ resLayoutPath,
573
+ resXmlPath,
574
+ resValuesPath,
575
+ resDrawablePath,
576
+ ].forEach((dir) => {
577
+ if (!fs.existsSync(dir)) {
578
+ fs.mkdirSync(dir, { recursive: true });
579
+ }
580
+ });
581
+ // Generate files for each widget
582
+ for (const w of resolvedProps.widgets) {
583
+ // Widget provider Kotlin
584
+ const providerCode = generateWidgetProviderCode(w, packageName, w.deepLinkUrl);
585
+ fs.writeFileSync(path.join(widgetPackagePath, `${w.name}Provider.kt`), providerCode);
586
+ // Layout XML
587
+ const layoutXml = generateWidgetLayoutXml(w);
588
+ fs.writeFileSync(path.join(resLayoutPath, `${w.name.toLowerCase()}_layout.xml`), layoutXml);
589
+ // List item layout (only for list-type widgets)
590
+ if (w.type === "list") {
591
+ const itemLayoutXml = generateWidgetItemLayoutXml(w);
592
+ fs.writeFileSync(path.join(resLayoutPath, `${w.name.toLowerCase()}_item.xml`), itemLayoutXml);
593
+ }
594
+ // Widget info XML
595
+ const widgetInfoXml = generateWidgetInfoXml(w, resolvedProps.android.updatePeriodMillis);
596
+ fs.writeFileSync(path.join(resXmlPath, `${w.name.toLowerCase()}_info.xml`), widgetInfoXml);
597
+ // Copy widget images to drawable (if image mode)
598
+ if (w.image) {
599
+ // Use the first available image as the drawable
600
+ const imgPath = w.image.medium || w.image.small || w.image.large;
601
+ if (imgPath) {
602
+ const imgSourcePath = path.isAbsolute(imgPath)
603
+ ? imgPath
604
+ : path.join(projectRoot, imgPath);
605
+ if (fs.existsSync(imgSourcePath)) {
606
+ const ext = path.extname(imgSourcePath);
607
+ const drawableName = `${w.name.toLowerCase()}_bg${ext}`;
608
+ fs.copyFileSync(imgSourcePath, path.join(resDrawablePath, drawableName));
609
+ console.log(`[nn-widgets] Copied Android widget image: ${drawableName}`);
610
+ }
611
+ else {
612
+ console.warn(`[nn-widgets] Android widget image not found: ${imgSourcePath}`);
613
+ }
614
+ }
615
+ }
616
+ // Copy widget icons to drawable (for list/single type)
617
+ if (w.icons) {
618
+ for (const [iconName, iconPath] of Object.entries(w.icons)) {
619
+ const imgSourcePath = path.isAbsolute(iconPath)
620
+ ? iconPath
621
+ : path.join(projectRoot, iconPath);
622
+ if (fs.existsSync(imgSourcePath)) {
623
+ const ext = path.extname(imgSourcePath);
624
+ const drawableName = `${iconName.toLowerCase()}${ext}`;
625
+ fs.copyFileSync(imgSourcePath, path.join(resDrawablePath, drawableName));
626
+ console.log(`[nn-widgets] Copied Android widget icon: ${drawableName}`);
627
+ }
628
+ else {
629
+ console.warn(`[nn-widgets] Android widget icon not found: ${imgSourcePath} (icon: ${iconName})`);
630
+ }
631
+ }
632
+ }
633
+ }
634
+ // Add string resources for all widgets
635
+ const stringsPath = path.join(resValuesPath, "strings.xml");
636
+ if (fs.existsSync(stringsPath)) {
637
+ let stringsContent = fs.readFileSync(stringsPath, "utf8");
638
+ for (const w of resolvedProps.widgets) {
639
+ const key = `${w.name.toLowerCase()}_description`;
640
+ if (!stringsContent.includes(key)) {
641
+ const widgetString = ` <string name="${key}">${w.description}</string>`;
642
+ stringsContent = stringsContent.replace("</resources>", `${widgetString}\n</resources>`);
643
+ }
644
+ }
645
+ fs.writeFileSync(stringsPath, stringsContent);
646
+ }
647
+ // Add receivers to manifest
648
+ const application = manifest.manifest.application?.[0];
649
+ if (application) {
650
+ if (!application.receiver) {
651
+ application.receiver = [];
652
+ }
653
+ for (const w of resolvedProps.widgets) {
654
+ const receiverName = `.widget.${w.name}Provider`;
655
+ const existingReceiver = application.receiver.find((r) => r.$?.["android:name"] === receiverName);
656
+ if (!existingReceiver) {
657
+ application.receiver.push({
658
+ $: {
659
+ "android:name": receiverName,
660
+ "android:exported": "true",
661
+ },
662
+ "intent-filter": [
663
+ {
664
+ action: [
665
+ {
666
+ $: {
667
+ "android:name": "android.appwidget.action.APPWIDGET_UPDATE",
668
+ },
669
+ },
670
+ ],
671
+ },
672
+ {
673
+ action: [
674
+ {
675
+ $: {
676
+ "android:name": `${packageName}.WIDGET_UPDATE_${w.name.toUpperCase()}`,
677
+ },
678
+ },
679
+ ],
680
+ },
681
+ ],
682
+ "meta-data": [
683
+ {
684
+ $: {
685
+ "android:name": "android.appwidget.provider",
686
+ "android:resource": `@xml/${w.name.toLowerCase()}_info`,
687
+ },
688
+ },
689
+ ],
690
+ });
691
+ console.log(`[nn-widgets] Added Android widget receiver: ${w.name}Provider`);
692
+ }
693
+ }
694
+ }
695
+ return config;
696
+ });
697
+ return config;
698
+ };
699
+ exports.withAndroidWidget = withAndroidWidget;
700
+ exports.default = exports.withAndroidWidget;