From 8dba7882f137037eb72f7e3f62b82022c1813eb9 Mon Sep 17 00:00:00 2001 From: ADAIBLOG <186315423+ADAIBLOG@users.noreply.github.com> Date: Sat, 20 Jun 2026 11:03:03 +0800 Subject: [PATCH 01/13] Create checkin_widget_info --- android/app/src/main/res/xml/checkin_widget_info | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 android/app/src/main/res/xml/checkin_widget_info diff --git a/android/app/src/main/res/xml/checkin_widget_info b/android/app/src/main/res/xml/checkin_widget_info new file mode 100644 index 0000000..5a505bc --- /dev/null +++ b/android/app/src/main/res/xml/checkin_widget_info @@ -0,0 +1,14 @@ + + + From 7164ff9f9ae2f57fa68fd00f96c34c653c08932f Mon Sep 17 00:00:00 2001 From: ADAIBLOG <186315423+ADAIBLOG@users.noreply.github.com> Date: Sat, 20 Jun 2026 11:03:19 +0800 Subject: [PATCH 02/13] Rename checkin_widget_info to checkin_widget_info.xml --- .../main/res/xml/{checkin_widget_info => checkin_widget_info.xml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename android/app/src/main/res/xml/{checkin_widget_info => checkin_widget_info.xml} (100%) diff --git a/android/app/src/main/res/xml/checkin_widget_info b/android/app/src/main/res/xml/checkin_widget_info.xml similarity index 100% rename from android/app/src/main/res/xml/checkin_widget_info rename to android/app/src/main/res/xml/checkin_widget_info.xml From e30debead1e8899253abea544c5eacae9513ffb3 Mon Sep 17 00:00:00 2001 From: ADAIBLOG <186315423+ADAIBLOG@users.noreply.github.com> Date: Sat, 20 Jun 2026 11:03:44 +0800 Subject: [PATCH 03/13] Create widget_checkin.xml --- .../src/main/res/layout/widget_checkin.xml | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 android/app/src/main/res/layout/widget_checkin.xml diff --git a/android/app/src/main/res/layout/widget_checkin.xml b/android/app/src/main/res/layout/widget_checkin.xml new file mode 100644 index 0000000..b1faa57 --- /dev/null +++ b/android/app/src/main/res/layout/widget_checkin.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + From 3a4d818b9288d346455ad77350adac2ef60551b6 Mon Sep 17 00:00:00 2001 From: ADAIBLOG <186315423+ADAIBLOG@users.noreply.github.com> Date: Sat, 20 Jun 2026 11:04:08 +0800 Subject: [PATCH 04/13] Create CheckInWidgetProvider.kt --- .../resurrect/grove/CheckInWidgetProvider.kt | 218 ++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 android/app/src/main/kotlin/com/resurrect/grove/CheckInWidgetProvider.kt diff --git a/android/app/src/main/kotlin/com/resurrect/grove/CheckInWidgetProvider.kt b/android/app/src/main/kotlin/com/resurrect/grove/CheckInWidgetProvider.kt new file mode 100644 index 0000000..00d1f35 --- /dev/null +++ b/android/app/src/main/kotlin/com/resurrect/grove/CheckInWidgetProvider.kt @@ -0,0 +1,218 @@ +package com.resurrect.grove + +import android.app.PendingIntent +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProvider +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.widget.RemoteViews +import org.json.JSONArray +import org.json.JSONObject +import java.text.SimpleDateFormat +import java.util.* + +class CheckInWidgetProvider : AppWidgetProvider() { + + companion object { + const val ACTION_TOGGLE_CHECKIN = "com.resurrect.grove.ACTION_CHECKIN_TOGGLE" + const val PREFS_NAME = "FlutterSharedPreferences" + const val NAV_PREFS = "grove_checkin_nav" + const val KEY_HABITS = "flutter.grove_v2_ids" + private const val LIST_PREFIX = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu" + } + + override fun onUpdate(ctx: Context, mgr: AppWidgetManager, ids: IntArray) { + ids.forEach { updateWidget(ctx, mgr, it) } + } + + override fun onReceive(ctx: Context, intent: Intent) { + super.onReceive(ctx, intent) + + val mgr = AppWidgetManager.getInstance(ctx) + + when (intent.action) { + ACTION_TOGGLE_CHECKIN -> { + val widgetId = intent.getIntExtra( + AppWidgetManager.EXTRA_APPWIDGET_ID, + AppWidgetManager.INVALID_APPWIDGET_ID + ) + if (widgetId != AppWidgetManager.INVALID_APPWIDGET_ID) { + toggleCheckIn(ctx, widgetId) + } + } + } + + val allIds = mgr.getAppWidgetIds(ComponentName(ctx, CheckInWidgetProvider::class.java)) + allIds.forEach { updateWidget(ctx, mgr, it) } + } + + private fun toggleCheckIn(ctx: Context, widgetId: Int) { + val flutterPrefs = ctx.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + val navPrefs = ctx.getSharedPreferences(NAV_PREFS, Context.MODE_PRIVATE) + + val habitIds = loadHabitIds(flutterPrefs) + if (habitIds.isEmpty()) return + + val selId = navPrefs.getString("checkin_habit_id_$widgetId", habitIds.firstOrNull()) + ?: return + + val habit = loadHabit(flutterPrefs, selId) ?: return + + val mode = habit.optInt("mode", 0) + if (mode != 1) return + + val todayKey = SimpleDateFormat("yyyy-MM-dd", Locale.US).format(Date()) + + val checkInDays = habit.optJSONArray("checkInDays") ?: JSONArray() + val existing = mutableListOf() + for (i in 0 until checkInDays.length()) { + val iso = checkInDays.optString(i, "") + if (iso.length >= 10) { + existing.add(iso.substring(0, 10)) + } + } + + val isCheckedInToday = existing.contains(todayKey) + + val newArray = JSONArray() + if (isCheckedInToday) { + for (day in existing) { + if (day != todayKey) { + newArray.put(day) + } + } + } else { + for (day in existing) { + newArray.put(day) + } + newArray.put(todayKey) + } + + habit.put("checkInDays", newArray) + flutterPrefs.edit().putString("flutter.$selId", habit.toString()).apply() + } + + private fun updateWidget(ctx: Context, mgr: AppWidgetManager, widgetId: Int) { + val flutterPrefs = ctx.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + val navPrefs = ctx.getSharedPreferences(NAV_PREFS, Context.MODE_PRIVATE) + + val habitIds = loadHabitIds(flutterPrefs) + val selId = navPrefs.getString("checkin_habit_id_$widgetId", habitIds.firstOrNull()) + val habit = selId?.let { loadHabit(flutterPrefs, it) } + + val views = RemoteViews(ctx.packageName, R.layout.widget_checkin) + + val habitName = habit?.optString("name", "—") ?: "Tap to pick" + val isCheckIn = (habit?.optInt("mode", 0) ?: 0) == 1 + val streak = if (isCheckIn) computeCheckInStreak(habit!!) else 0 + + val todayKey = SimpleDateFormat("yyyy-MM-dd", Locale.US).format(Date()) + val checkInDays = habit?.optJSONArray("checkInDays") + var checkedToday = false + if (checkInDays != null && isCheckIn) { + for (i in 0 until checkInDays.length()) { + val iso = checkInDays.optString(i, "") + if (iso.length >= 10 && iso.substring(0, 10) == todayKey) { + checkedToday = true + break + } + } + } + + views.setTextViewText(R.id.checkin_habit_name, habitName) + views.setTextViewText(R.id.checkin_streak, "$streak") + + if (isCheckIn) { + val color = habit?.optInt("color", 0xFF4E8B5F.toInt()) ?: 0xFF4E8B5F.toInt() + val alpha = if (checkedToday) color or (0xFF shl 24) else (0x44E0EBE0.toInt()) + + views.setTextViewText(R.id.checkin_icon, if (checkedToday) "✓" else "○") + views.setInt(R.id.checkin_icon, "setTextColor", alpha.toInt()) + } else { + views.setTextViewText(R.id.checkin_icon, "—") + views.setInt(R.id.checkin_icon, "setTextColor", 0x448AA88C.toInt()) + } + + views.setOnClickPendingIntent( + R.id.checkin_icon, + makeBroadcast(ctx, ACTION_TOGGLE_CHECKIN, widgetId, widgetId * 10 + 1) + ) + + val openApp = PendingIntent.getActivity( + ctx, widgetId * 10 + 2, + ctx.packageManager.getLaunchIntentForPackage(ctx.packageName), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + views.setOnClickPendingIntent(R.id.checkin_root, openApp) + + mgr.updateAppWidget(widgetId, views) + } + + private fun computeCheckInStreak(habit: JSONObject): Int { + val arr = habit.optJSONArray("checkInDays") ?: return 0 + if (arr.length() == 0) return 0 + + val days = mutableSetOf() + for (i in 0 until arr.length()) { + val iso = arr.optString(i, "") + if (iso.length >= 10) days.add(iso.substring(0, 10)) + } + + val cal = Calendar.getInstance() + var streak = 0 + val sdf = SimpleDateFormat("yyyy-MM-dd", Locale.US) + + while (true) { + val key = sdf.format(cal.time) + if (days.contains(key)) { + streak++ + cal.add(Calendar.DAY_OF_YEAR, -1) + } else { + break + } + } + return streak + } + + private fun makeBroadcast(ctx: Context, action: String, + widgetId: Int, requestCode: Int): PendingIntent { + val intent = Intent(action).apply { + component = ComponentName(ctx, CheckInWidgetProvider::class.java) + putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId) + } + return PendingIntent.getBroadcast( + ctx, requestCode, intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + } + + private fun loadHabitIds(prefs: android.content.SharedPreferences): List { + try { + val set = prefs.getStringSet(KEY_HABITS, null) + if (!set.isNullOrEmpty()) return set.toList() + } catch (_: ClassCastException) {} + + val raw = prefs.getString(KEY_HABITS, null) ?: return emptyList() + + if (raw.startsWith(LIST_PREFIX)) { + val bang = raw.indexOf('!') + if (bang >= 0) { + return try { + val arr = JSONArray(raw.substring(bang + 1)) + (0 until arr.length()).map { arr.getString(it) } + } catch (_: Exception) { emptyList() } + } + } + + return try { + val arr = JSONArray(raw) + (0 until arr.length()).map { arr.getString(it) } + } catch (_: Exception) { emptyList() } + } + + private fun loadHabit(prefs: android.content.SharedPreferences, id: String): JSONObject? { + val json = prefs.getString("flutter.$id", null) ?: return null + return try { JSONObject(json) } catch (_: Exception) { null } + } +} From 14c6341d75ae61ee3d59de416c354dc23c93d058 Mon Sep 17 00:00:00 2001 From: ADAIBLOG <186315423+ADAIBLOG@users.noreply.github.com> Date: Sat, 20 Jun 2026 11:05:15 +0800 Subject: [PATCH 05/13] Update strings.xml --- android/app/src/main/res/values/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 64fb728..b1cd9ec 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -3,4 +3,5 @@ Grove Grove monthly calendar Grove habit tree + Grove quick check-in From 6f20e3041f367a21682e346ddde4e18818151371 Mon Sep 17 00:00:00 2001 From: ADAIBLOG <186315423+ADAIBLOG@users.noreply.github.com> Date: Sat, 20 Jun 2026 11:05:31 +0800 Subject: [PATCH 06/13] Update AndroidManifest.xml --- android/app/src/main/AndroidManifest.xml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 6161e56..5ac3517 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -89,6 +89,19 @@ android:resource="@xml/tree_widget_info" /> + + + + + + + + + From 661ef845c8193e02123072c1c55a179ec62ec8a3 Mon Sep 17 00:00:00 2001 From: ADAIBLOG <186315423+ADAIBLOG@users.noreply.github.com> Date: Sat, 20 Jun 2026 11:05:56 +0800 Subject: [PATCH 07/13] Update MainActivity.kt --- .../app/src/main/kotlin/com/resurrect/grove/MainActivity.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/kotlin/com/resurrect/grove/MainActivity.kt b/android/app/src/main/kotlin/com/resurrect/grove/MainActivity.kt index 0e37a00..8977a05 100644 --- a/android/app/src/main/kotlin/com/resurrect/grove/MainActivity.kt +++ b/android/app/src/main/kotlin/com/resurrect/grove/MainActivity.kt @@ -59,7 +59,8 @@ class MainActivity : FlutterFragmentActivity() { val mgr = AppWidgetManager.getInstance(this) listOf( CalendarWidgetProvider::class.java, - TreeWidgetProvider::class.java + TreeWidgetProvider::class.java, + CheckInWidgetProvider::class.java ).forEach { cls -> val ids = mgr.getAppWidgetIds(ComponentName(this, cls)) if (ids.isNotEmpty()) { From 9332266704ead6231d797a967763aa682231e624 Mon Sep 17 00:00:00 2001 From: ADAIBLOG <186315423+ADAIBLOG@users.noreply.github.com> Date: Sat, 20 Jun 2026 11:31:13 +0800 Subject: [PATCH 08/13] Update checkin_widget_info.xml --- android/app/src/main/res/xml/checkin_widget_info.xml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/android/app/src/main/res/xml/checkin_widget_info.xml b/android/app/src/main/res/xml/checkin_widget_info.xml index 5a505bc..8d63bb6 100644 --- a/android/app/src/main/res/xml/checkin_widget_info.xml +++ b/android/app/src/main/res/xml/checkin_widget_info.xml @@ -1,14 +1,15 @@ From 3dae2b4a476f2ae4a2984a1135ad9ef7c5602728 Mon Sep 17 00:00:00 2001 From: ADAIBLOG <186315423+ADAIBLOG@users.noreply.github.com> Date: Sat, 20 Jun 2026 11:45:19 +0800 Subject: [PATCH 09/13] Update checkin_widget_info.xml --- android/app/src/main/res/xml/checkin_widget_info.xml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/android/app/src/main/res/xml/checkin_widget_info.xml b/android/app/src/main/res/xml/checkin_widget_info.xml index 8d63bb6..5a505bc 100644 --- a/android/app/src/main/res/xml/checkin_widget_info.xml +++ b/android/app/src/main/res/xml/checkin_widget_info.xml @@ -1,15 +1,14 @@ From 7591dc9033fab1b7fa4900cfc757baad8cc559f0 Mon Sep 17 00:00:00 2001 From: ADAIBLOG <186315423+ADAIBLOG@users.noreply.github.com> Date: Sat, 20 Jun 2026 12:01:30 +0800 Subject: [PATCH 10/13] Update CheckInWidgetProvider.kt --- .../kotlin/com/resurrect/grove/CheckInWidgetProvider.kt | 7 ------- 1 file changed, 7 deletions(-) diff --git a/android/app/src/main/kotlin/com/resurrect/grove/CheckInWidgetProvider.kt b/android/app/src/main/kotlin/com/resurrect/grove/CheckInWidgetProvider.kt index 00d1f35..5f61b41 100644 --- a/android/app/src/main/kotlin/com/resurrect/grove/CheckInWidgetProvider.kt +++ b/android/app/src/main/kotlin/com/resurrect/grove/CheckInWidgetProvider.kt @@ -139,13 +139,6 @@ class CheckInWidgetProvider : AppWidgetProvider() { makeBroadcast(ctx, ACTION_TOGGLE_CHECKIN, widgetId, widgetId * 10 + 1) ) - val openApp = PendingIntent.getActivity( - ctx, widgetId * 10 + 2, - ctx.packageManager.getLaunchIntentForPackage(ctx.packageName), - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - views.setOnClickPendingIntent(R.id.checkin_root, openApp) - mgr.updateAppWidget(widgetId, views) } From b3ad9f55eb704b4e6a399c253f2245be21c0ae61 Mon Sep 17 00:00:00 2001 From: ADAIBLOG <186315423+ADAIBLOG@users.noreply.github.com> Date: Sat, 20 Jun 2026 12:01:58 +0800 Subject: [PATCH 11/13] Update widget_bridge.dart --- lib/services/widget_bridge.dart | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/lib/services/widget_bridge.dart b/lib/services/widget_bridge.dart index a63c875..dec9ab9 100644 --- a/lib/services/widget_bridge.dart +++ b/lib/services/widget_bridge.dart @@ -13,24 +13,27 @@ class GroveWidgetBridge { Future renderAndUpdate(List habits) async { try { final filesDir = await _channel.invokeMethod('getFilesDir'); - if (filesDir == null) return; + if (filesDir != null) { + for (final habit in habits) { + try { + final bytes = await _renderHabit(habit); + if (bytes == null) continue; - for (final habit in habits) { - final bytes = await _renderHabit(habit); - if (bytes == null) continue; - - await _channel.invokeMethod('saveTreeImage', { - 'habitId': habit.id, - 'bytes': bytes, - }); + await _channel.invokeMethod('saveTreeImage', { + 'habitId': habit.id, + 'bytes': bytes, + }); + } catch (e) { + debugPrint('GroveWidgetBridge: failed to render habit ${habit.id}: $e'); + } + } } - - await _channel.invokeMethod('updateWidgets'); } on MissingPluginException { - } catch (e) { debugPrint('GroveWidgetBridge.renderAndUpdate error: $e'); } + + await requestUpdate(); } Future requestUpdate() async { From a558a8b8903929d4e975ff90037a28f4720c8a4d Mon Sep 17 00:00:00 2001 From: ADAIBLOG <186315423+ADAIBLOG@users.noreply.github.com> Date: Sat, 20 Jun 2026 12:02:25 +0800 Subject: [PATCH 12/13] Update grove_model.dart --- lib/providers/grove_model.dart | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/providers/grove_model.dart b/lib/providers/grove_model.dart index e5ea0fd..54448fb 100644 --- a/lib/providers/grove_model.dart +++ b/lib/providers/grove_model.dart @@ -5,7 +5,7 @@ import 'package:grove/models/grove_models.dart'; import 'package:grove/services/grove_notifications.dart'; import 'package:grove/services/widget_bridge.dart'; -class GroveModel extends ChangeNotifier { +class GroveModel extends ChangeNotifier with WidgetsBindingObserver { List _habits = []; List get habits => List.unmodifiable(_habits); @@ -16,11 +16,30 @@ class GroveModel extends ChangeNotifier { try { _prefs = await SharedPreferences.getInstance(); _load(); + WidgetsBinding.instance.addObserver(this); } catch (e) { debugPrint('GroveModel init error: $e'); } } + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + _reloadFromPrefs(); + } + } + + void _reloadFromPrefs() { + if (_prefs == null) return; + _load(); + } + void _load() { if (_prefs == null) return; final ids = _prefs!.getStringList(_idsKey) ?? []; From e384f3a7f9ec16932ee1802b1656329b54840557 Mon Sep 17 00:00:00 2001 From: ADAIBLOG <186315423+ADAIBLOG@users.noreply.github.com> Date: Sat, 20 Jun 2026 12:30:24 +0800 Subject: [PATCH 13/13] Update grove_model.dart --- lib/providers/grove_model.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/providers/grove_model.dart b/lib/providers/grove_model.dart index 54448fb..ed3e326 100644 --- a/lib/providers/grove_model.dart +++ b/lib/providers/grove_model.dart @@ -35,8 +35,9 @@ class GroveModel extends ChangeNotifier with WidgetsBindingObserver { } } - void _reloadFromPrefs() { + Future _reloadFromPrefs() async { if (_prefs == null) return; + await _prefs!.reload(); _load(); }