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.
- package/README.md +577 -0
- package/android/build.gradle +90 -0
- package/app.plugin.js +4 -0
- package/build/NNWidgets.types.d.ts +113 -0
- package/build/NNWidgets.types.d.ts.map +1 -0
- package/build/NNWidgets.types.js +2 -0
- package/build/NNWidgets.types.js.map +1 -0
- package/build/NNWidgetsModule.d.ts +3 -0
- package/build/NNWidgetsModule.d.ts.map +1 -0
- package/build/NNWidgetsModule.js +3 -0
- package/build/NNWidgetsModule.js.map +1 -0
- package/build/index.d.ts +11 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +152 -0
- package/build/index.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/NNWidgets.podspec +27 -0
- package/ios/NNWidgetsModule.swift +97 -0
- package/package.json +49 -0
- package/plugin/build/index.d.ts +9 -0
- package/plugin/build/index.d.ts.map +1 -0
- package/plugin/build/index.js +70 -0
- package/plugin/build/types.d.ts +353 -0
- package/plugin/build/types.d.ts.map +1 -0
- package/plugin/build/types.js +2 -0
- package/plugin/build/withAndroidWidget.d.ts +5 -0
- package/plugin/build/withAndroidWidget.d.ts.map +1 -0
- package/plugin/build/withAndroidWidget.js +700 -0
- package/plugin/build/withIosWidget.d.ts +6 -0
- package/plugin/build/withIosWidget.d.ts.map +1 -0
- package/plugin/build/withIosWidget.js +1589 -0
- package/plugin/tsconfig.tsbuildinfo +1 -0
- package/publish.sh +59 -0
|
@@ -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;
|