From 5b8d2c95c2beddb8aa7e003d68f3fe99a031449c Mon Sep 17 00:00:00 2001 From: chan9an Date: Sun, 1 Mar 2026 13:44:51 +0530 Subject: [PATCH 1/5] refactor: simplify Android widget deep linking by directly launching MainActivity --- .../TaskWarriorWidgetProvider.kt | 381 +++++++++--------- .../home/controllers/home_controller.dart | 23 +- .../splash/controllers/splash_controller.dart | 27 +- lib/app/services/deep_link_service.dart | 31 +- lib/main.dart | 17 +- 5 files changed, 253 insertions(+), 226 deletions(-) diff --git a/android/app/src/main/kotlin/com/ccextractor/taskwarriorflutter/TaskWarriorWidgetProvider.kt b/android/app/src/main/kotlin/com/ccextractor/taskwarriorflutter/TaskWarriorWidgetProvider.kt index d57d4cb0..157413c8 100644 --- a/android/app/src/main/kotlin/com/ccextractor/taskwarriorflutter/TaskWarriorWidgetProvider.kt +++ b/android/app/src/main/kotlin/com/ccextractor/taskwarriorflutter/TaskWarriorWidgetProvider.kt @@ -1,239 +1,228 @@ package com.ccextractor.taskwarriorflutter + import android.annotation.TargetApi +import android.app.PendingIntent import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProvider import android.content.Context +import android.content.Intent import android.net.Uri +import android.os.Build import android.widget.RemoteViews -import es.antonborri.home_widget.HomeWidgetBackgroundIntent +import android.widget.RemoteViewsService import es.antonborri.home_widget.HomeWidgetLaunchIntent -import es.antonborri.home_widget.HomeWidgetProvider import es.antonborri.home_widget.HomeWidgetPlugin +import org.json.JSONArray as OrgJSONArray import org.json.JSONException -import android.content.Intent -import android.widget.RemoteViewsService import org.json.JSONObject -import org.json.JSONArray as OrgJSONArray -import android.os.Bundle -import android.app.PendingIntent -import android.appwidget.AppWidgetProvider -import android.os.Build - @TargetApi(Build.VERSION_CODES.CUPCAKE) class TaskWarriorWidgetProvider : AppWidgetProvider() { - override fun onReceive(context: Context, intent: Intent) { - // Handle the custom action from your Widget buttons/list - if (intent.action == "TASK_ACTION") { - val uuid = intent.getStringExtra("uuid") ?: "" - val launchedFor = intent.getStringExtra("launchedFor") - - // 1. Construct the URI exactly as Flutter expects it - // Scheme: taskwarrior:// - // Host: cardclicked OR addclicked - val deepLinkUri = if (launchedFor == "ADD_TASK") { - Uri.parse("taskwarrior://addclicked") - } else { - // For list items, we attach the UUID - Uri.parse("taskwarrior://cardclicked?uuid=$uuid") - } - - // 2. Create the Intent to open MainActivity - val launchIntent = Intent(context, MainActivity::class.java).apply { - action = Intent.ACTION_VIEW - data = deepLinkUri - // These flags ensure the app opens correctly whether running or not - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP - } - - context.startActivity(launchIntent) - } - super.onReceive(context, intent) - } - fun getLayoutId(context: Context) : Int{ - val sharedPrefs = HomeWidgetPlugin.getData(context) - val theme = sharedPrefs.getString("themeMode", "") - val layoutId = if (theme.equals("dark")) { - R.layout.taskwarrior_layout_dark // Define a dark mode layout in your resources - } else { - R.layout.taskwarrior_layout - } - return layoutId - } -@TargetApi(Build.VERSION_CODES.DONUT) -override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { - appWidgetIds.forEach { widgetId -> - // 1. Get the latest data from HomeWidget/SharedPrefs + fun getLayoutId(context: Context): Int { val sharedPrefs = HomeWidgetPlugin.getData(context) - val tasks = sharedPrefs.getString("tasks", "") - - // 2. Create the Intent for the ListView service - // We add the widgetId to the data URI to make it unique, preventing caching issues - val intent = Intent(context, ListViewRemoteViewsService::class.java).apply { - putExtra("tasksJsonString", tasks) - data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME) + widgetId) - } - - // 3. Initialize RemoteViews with the THEMED layout (getLayoutId handles dark/light logic) - val views = RemoteViews(context.packageName, getLayoutId(context)).apply { - - // Set up the Logo click (Open App) - val pendingIntent: PendingIntent = HomeWidgetLaunchIntent.getActivity( - context, - MainActivity::class.java - ) - setOnClickPendingIntent(R.id.logo, pendingIntent) - - // Set up the Add Button click (Custom Action) - val intent_for_add = Intent(context, TaskWarriorWidgetProvider::class.java).apply { - action = "TASK_ACTION" - putExtra("launchedFor", "ADD_TASK") - // Unique data to ensure the broadcast is fresh - data = Uri.parse("taskwarrior://addtask/$widgetId") - } - - val pendingIntentAdd: PendingIntent = PendingIntent.getBroadcast( - context, - widgetId, - intent_for_add, - PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT - ) - setOnClickPendingIntent(R.id.add_btn, pendingIntentAdd) - - // Attach the adapter to the ListView - setRemoteAdapter(R.id.list_view, intent) - } - - // 4. Set up the Click Template for List Items (Deep Linking) - val clickPendingIntent: PendingIntent = Intent( - context, - TaskWarriorWidgetProvider::class.java - ).run { - action = "TASK_ACTION" - // Important: Use widgetId as requestCode to keep it unique - PendingIntent.getBroadcast( - context, - widgetId, - this, - PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT - ) + val theme = sharedPrefs.getString("themeMode", "") + val layoutId = + if (theme.equals("dark")) { + R.layout.taskwarrior_layout_dark // Define a dark mode layout in your resources + } else { + R.layout.taskwarrior_layout + } + return layoutId + } + @TargetApi(Build.VERSION_CODES.DONUT) + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray + ) { + appWidgetIds.forEach { widgetId -> + // 1. Get the latest data from HomeWidget/SharedPrefs + val sharedPrefs = HomeWidgetPlugin.getData(context) + val tasks = sharedPrefs.getString("tasks", "") + + // 2. Create the Intent for the ListView service + // We add the widgetId to the data URI to make it unique, preventing caching issues + val intent = + Intent(context, ListViewRemoteViewsService::class.java).apply { + putExtra("tasksJsonString", tasks) + data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME) + widgetId) + } + + // 3. Initialize RemoteViews with the THEMED layout (getLayoutId handles dark/light + // logic) + val views = + RemoteViews(context.packageName, getLayoutId(context)).apply { + + // Set up the Logo click (Open App) + val pendingIntent: PendingIntent = + HomeWidgetLaunchIntent.getActivity( + context, + MainActivity::class.java + ) + setOnClickPendingIntent(R.id.logo, pendingIntent) + + // Set up the Add Button click (Direct to MainActivity) + val intentForAdd = + Intent(context, MainActivity::class.java).apply { + action = Intent.ACTION_VIEW + data = Uri.parse("taskwarrior://addclicked") + flags = + Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_CLEAR_TOP or + Intent.FLAG_ACTIVITY_SINGLE_TOP + } + + val pendingIntentAdd: PendingIntent = + PendingIntent.getActivity( + context, + widgetId, + intentForAdd, + PendingIntent.FLAG_IMMUTABLE or + PendingIntent.FLAG_UPDATE_CURRENT + ) + setOnClickPendingIntent(R.id.add_btn, pendingIntentAdd) + + // Attach the adapter to the ListView + setRemoteAdapter(R.id.list_view, intent) + } + + // 4. Set up the Click Template for List Items (Deep Linking) + val clickIntentTemplate = + Intent(context, MainActivity::class.java).apply { + action = Intent.ACTION_VIEW + flags = + Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_CLEAR_TOP or + Intent.FLAG_ACTIVITY_SINGLE_TOP + } + val clickPendingIntentTemplate: PendingIntent = + PendingIntent.getActivity( + context, + widgetId, + clickIntentTemplate, + PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) + views.setPendingIntentTemplate(R.id.list_view, clickPendingIntentTemplate) + + // 5. THE THEME FIX: Notify the manager that the list data/layout needs a refresh + appWidgetManager.notifyAppWidgetViewDataChanged(widgetId, R.id.list_view) + + // 6. Push the update to the widget + appWidgetManager.updateAppWidget(widgetId, views) } - views.setPendingIntentTemplate(R.id.list_view, clickPendingIntent) - - // 5. THE THEME FIX: Notify the manager that the list data/layout needs a refresh - appWidgetManager.notifyAppWidgetViewDataChanged(widgetId, R.id.list_view) - - // 6. Push the update to the widget - appWidgetManager.updateAppWidget(widgetId, views) + super.onUpdate(context, appWidgetManager, appWidgetIds) } - super.onUpdate(context, appWidgetManager, appWidgetIds) -} } +} + class ListViewRemoteViewsFactory( - private val context: Context, - private val tasksJsonString: String? + private val context: Context, + private val tasksJsonString: String? ) : RemoteViewsService.RemoteViewsFactory { private val tasks = mutableListOf() override fun onCreate() {} - override fun onDataSetChanged() { - tasks.clear() // Add this! - val sharedPrefs = HomeWidgetPlugin.getData(context) - val latestTasksJson = sharedPrefs.getString("tasks", "") - - if (!latestTasksJson.isNullOrEmpty()) { - try { - val jsonArray = OrgJSONArray(latestTasksJson) - for (i in 0 until jsonArray.length()) { - tasks.add(Task.fromJson(jsonArray.getJSONObject(i))) - } - } catch (e: JSONException) { - e.printStackTrace() - } - } - } + override fun onDataSetChanged() { + tasks.clear() // Add this! + val sharedPrefs = HomeWidgetPlugin.getData(context) + val latestTasksJson = sharedPrefs.getString("tasks", "") + + if (!latestTasksJson.isNullOrEmpty()) { + try { + val jsonArray = OrgJSONArray(latestTasksJson) + for (i in 0 until jsonArray.length()) { + tasks.add(Task.fromJson(jsonArray.getJSONObject(i))) + } + } catch (e: JSONException) { + e.printStackTrace() + } + } + } override fun onDestroy() {} override fun getCount(): Int = tasks.size - fun getListItemLayoutId(): Int{ - val sharedPrefs = HomeWidgetPlugin.getData(context) - val theme = sharedPrefs.getString("themeMode", "") - val layoutId = if (theme.equals("dark")) { - R.layout.listitem_layout_dark // Define a dark mode layout in your resources - } else { - R.layout.listitem_layout - } - return layoutId - } - fun getListItemLayoutIdForR1(): Int{ - val sharedPrefs = HomeWidgetPlugin.getData(context) - val theme = sharedPrefs.getString("themeMode", "") - val layoutId = if (theme.equals("dark")) { - R.layout.no_tasks_found_li_dark // Define a dark mode layout in your resources - } else { - R.layout.no_tasks_found_li - } - return layoutId - } - fun getDotIdByPriority(p: String) : Int{ - println("PRIORITY: "+p) - if(p.equals("L")) return R.drawable.low_priority_dot - if(p.equals("M")) return R.drawable.mid_priority_dot - if(p.equals("H")) return R.drawable.high_priority_dot - return R.drawable.no_priority_dot - } + fun getListItemLayoutId(): Int { + val sharedPrefs = HomeWidgetPlugin.getData(context) + val theme = sharedPrefs.getString("themeMode", "") + val layoutId = + if (theme.equals("dark")) { + R.layout.listitem_layout_dark // Define a dark mode layout in your resources + } else { + R.layout.listitem_layout + } + return layoutId + } + fun getListItemLayoutIdForR1(): Int { + val sharedPrefs = HomeWidgetPlugin.getData(context) + val theme = sharedPrefs.getString("themeMode", "") + val layoutId = + if (theme.equals("dark")) { + R.layout.no_tasks_found_li_dark // Define a dark mode layout in your resources + } else { + R.layout.no_tasks_found_li + } + return layoutId + } + fun getDotIdByPriority(p: String): Int { + println("PRIORITY: " + p) + if (p.equals("L")) return R.drawable.low_priority_dot + if (p.equals("M")) return R.drawable.mid_priority_dot + if (p.equals("H")) return R.drawable.high_priority_dot + return R.drawable.no_priority_dot + } override fun getViewAt(position: Int): RemoteViews { val task = tasks[position] - if(task.uuid.equals("NO_TASK")) - return RemoteViews(context.packageName, getListItemLayoutIdForR1()).apply { - if(task.priority.equals("1")) - setTextViewText(R.id.tv, "No tasks added yet") - if(task.priority.equals("2")) - setTextViewText(R.id.tv, "Filters applied are hiding all tasks") - } - return RemoteViews(context.packageName, getListItemLayoutId()).apply { - setTextViewText(R.id.todo__title, task.title) - setImageViewResource(R.id.dot, getDotIdByPriority(task.priority)) - val a = Intent().apply { - - Bundle().also { extras -> - extras.putString("action", "show_task") - extras.putString("uuid", tasks[position].uuid) - putExtras(extras) - } - - } - setOnClickFillInIntent(R.id.list_item_container,a) - } - + if (task.uuid.equals("NO_TASK")) + return RemoteViews(context.packageName, getListItemLayoutIdForR1()).apply { + if (task.priority.equals("1")) setTextViewText(R.id.tv, "No tasks added yet") + if (task.priority.equals("2")) + setTextViewText(R.id.tv, "Filters applied are hiding all tasks") + } + return RemoteViews(context.packageName, getListItemLayoutId()).apply { + setTextViewText(R.id.todo__title, task.title) + setImageViewResource(R.id.dot, getDotIdByPriority(task.priority)) + val fillInIntent = + Intent().apply { + data = Uri.parse("taskwarrior://cardclicked?uuid=${task.uuid}") + } + setOnClickFillInIntent(R.id.list_item_container, fillInIntent) + } } override fun getLoadingView(): RemoteViews? = null - override fun getViewTypeCount(): Int = 2 + override fun getViewTypeCount(): Int = 2 override fun getItemId(position: Int): Long = position.toLong() override fun hasStableIds(): Boolean = true } + class ListViewRemoteViewsService : RemoteViewsService() { - override fun onGetViewFactory(intent: Intent): RemoteViewsFactory { - val tasksJsonString = intent.getStringExtra("tasksJsonString") - return ListViewRemoteViewsFactory(applicationContext, tasksJsonString) - } + override fun onGetViewFactory(intent: Intent): RemoteViewsFactory { + val tasksJsonString = intent.getStringExtra("tasksJsonString") + return ListViewRemoteViewsFactory(applicationContext, tasksJsonString) + } +} + +data class Task( + val title: String, + val urgencyLevel: String, + val uuid: String, + val priority: String +) { + companion object { + fun fromJson(json: JSONObject): Task { + val title = json.optString("description", "") + val urgencyLevel = json.optString("urgency", "") + val uuid = json.optString("uuid", "") + val priority = json.optString("priority", "") + return Task(title, urgencyLevel, uuid, priority) + } + } } -data class Task(val title: String, val urgencyLevel: String,val uuid:String, val priority: String) { - companion object { - fun fromJson(json: JSONObject): Task { - val title = json.optString("description", "") - val urgencyLevel = json.optString("urgency", "") - val uuid = json.optString("uuid","") - val priority = json.optString("priority", "") - return Task(title, urgencyLevel, uuid, priority) - } - } -} \ No newline at end of file diff --git a/lib/app/modules/home/controllers/home_controller.dart b/lib/app/modules/home/controllers/home_controller.dart index 053f2bcc..e0723672 100644 --- a/lib/app/modules/home/controllers/home_controller.dart +++ b/lib/app/modules/home/controllers/home_controller.dart @@ -12,8 +12,8 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:taskwarrior/app/models/filters.dart'; import 'package:taskwarrior/app/models/json/task.dart'; -import 'package:taskwarrior/app/models/storage.dart'; import 'package:taskwarrior/app/models/storage/client.dart'; +import 'package:taskwarrior/app/models/storage.dart'; import 'package:taskwarrior/app/models/tag_meta_data.dart'; import 'package:taskwarrior/app/modules/home/controllers/widget.controller.dart'; import 'package:taskwarrior/app/modules/splash/controllers/splash_controller.dart'; @@ -70,6 +70,7 @@ class HomeController extends GetxController { @override void onInit() { + debugPrint("🚀 BOOT: HomeController.onInit()"); super.onInit(); storage = Storage( Directory( @@ -78,6 +79,7 @@ class HomeController extends GetxController { ); serverCertExists = RxBool(storage.guiPemFiles.serverCertExists()); addListenerToScrollController(); + _profileSet(); loadDelayTask(); initLanguageAndDarkMode(); @@ -127,9 +129,19 @@ class HomeController extends GetxController { @override void onReady() { super.onReady(); - if (Get.isRegistered()) { - Get.find().consumePendingActions(this); - } + // Automatically check for any queued Deep Links when Home spins up. + // We delay slightly to ensure the Navigator route swap finishes first, avoiding widget tree lock. + Future.delayed(const Duration(milliseconds: 50), () { + if (isClosed) return; + if (Get.isRegistered()) { + final deepLinkService = Get.find(); + if (deepLinkService.queuedUri != null) { + debugPrint( + "TRACE: HomeController.onReady() consuming deferred queue!"); + deepLinkService.consumePendingActions(this); + } + } + }); } Future> getUniqueProjects() async { @@ -577,8 +589,7 @@ class HomeController extends GetxController { await synchronize(context, false); } if (context.mounted) { - final tColors = - Theme.of(context).extension()!; + final tColors = Theme.of(context).extension()!; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( diff --git a/lib/app/modules/splash/controllers/splash_controller.dart b/lib/app/modules/splash/controllers/splash_controller.dart index 126aaebb..b1e3742d 100644 --- a/lib/app/modules/splash/controllers/splash_controller.dart +++ b/lib/app/modules/splash/controllers/splash_controller.dart @@ -13,6 +13,7 @@ import 'package:taskwarrior/app/routes/app_pages.dart'; import 'package:taskwarrior/app/utils/taskchampion/credentials_storage.dart'; import 'package:taskwarrior/app/utils/taskfunctions/profiles.dart'; import 'package:taskwarrior/app/v3/models/task.dart'; +import 'package:taskwarrior/app/services/deep_link_service.dart'; class SplashController extends GetxController { late Rx baseDirectory = Directory('').obs; @@ -23,14 +24,26 @@ class SplashController extends GetxController { @override void onInit() async { + debugPrint("🚀 BOOT: SplashController.onInit()"); super.onInit(); + + // If we don't, HomeController will boot blind and crash trying to read empty paths. + await initBaseDir(); + _checkProfiles(); + profilesMap.value = _profiles.profilesMap(); + currentProfile.value = _profiles.getCurrentProfile()!; + + // FIX 2: NOW we check if we should bypass the slow UI stuff. + final deepLinkService = Get.find(); + if (deepLinkService.queuedUri != null) { + debugPrint("🚀 TRACE: Bypassing Splash routing for queued URI"); + Get.offNamed(Routes.HOME); + return; // Skip the slow app updates and onboarding checks + } + + // Normal boot sequence for people just opening the app normally await checkForUpdate(); - initBaseDir().then((_) { - _checkProfiles(); - profilesMap.value = _profiles.profilesMap(); - currentProfile.value = _profiles.getCurrentProfile()!; - sendToNextPage(); - }); + sendToNextPage(); } Future initBaseDir() async { @@ -160,4 +173,4 @@ class SplashController extends GetxController { debugPrint(e.toString()); } } -} +} \ No newline at end of file diff --git a/lib/app/services/deep_link_service.dart b/lib/app/services/deep_link_service.dart index f016c93c..5d72fa13 100644 --- a/lib/app/services/deep_link_service.dart +++ b/lib/app/services/deep_link_service.dart @@ -7,16 +7,21 @@ import 'package:taskwarrior/app/routes/app_pages.dart'; class DeepLinkService extends GetxService { late AppLinks _appLinks; - Uri? _queuedUri; + String? queuedUri; - @override - void onReady() { - super.onReady(); - _initDeepLinks(); - } - - void _initDeepLinks() { + Future init() async { _appLinks = AppLinks(); + + try { + final initialUri = await _appLinks.getInitialLink(); + if (initialUri != null) { + queuedUri = initialUri.toString(); + debugPrint('🔗 INITIAL LINK QUEUED: $queuedUri'); + } + } catch (e) { + debugPrint('Deep link init error (safe to ignore on unsupported platforms): $e'); + } + _appLinks.uriLinkStream.listen((uri) { debugPrint('🔗 LINK RECEIVED: $uri'); _handleWidgetUri(uri); @@ -28,15 +33,15 @@ class DeepLinkService extends GetxService { _executeAction(uri, Get.find()); } else { debugPrint("⏳ HomeController not ready. Queuing action."); - _queuedUri = uri; + queuedUri = uri.toString(); } } void consumePendingActions(HomeController controller) { - if (_queuedUri != null) { + if (queuedUri != null) { debugPrint("🚀 Executing queued action..."); - _executeAction(_queuedUri!, controller); - _queuedUri = null; + _executeAction(Uri.parse(queuedUri!), controller); + queuedUri = null; } } @@ -66,4 +71,4 @@ class DeepLinkService extends GetxService { } } } -} +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 09f0268c..9f00e179 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,8 +3,6 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -// 1. Add this import -import 'package:app_links/app_links.dart'; import 'package:taskwarrior/app/services/deep_link_service.dart'; import 'package:taskwarrior/app/utils/app_settings/app_settings.dart'; @@ -29,6 +27,9 @@ DynamicLibrary loadNativeLibrary() { } void main() async { + debugPrint("🚀 BOOT: main() started"); + WidgetsFlutterBinding.ensureInitialized(); + debugPrint = (String? message, {int? wrapWidth}) { if (message != null) { debugPrintSynchronously(message, wrapWidth: wrapWidth); @@ -39,16 +40,24 @@ void main() async { loadNativeLibrary(); await RustLib.init(); - WidgetsFlutterBinding.ensureInitialized(); await AppSettings.init(); - Get.put(DeepLinkService(), permanent: true); + // fix: Actually await the service initialization so the OS intent is caught BEFORE runApp. + await Get.putAsync(() async { + final service = DeepLinkService(); + await service.init(); + return service; + }, permanent: true); runApp( GetMaterialApp( darkTheme: darkTheme, theme: lightTheme, title: "Application", initialRoute: AppPages.INITIAL, + unknownRoute: AppPages.routes.firstWhere( + (page) => page.name == AppPages.INITIAL, + orElse: () => AppPages.routes.first, + ), getPages: AppPages.routes, themeMode: AppSettings.isDarkMode ? ThemeMode.dark : ThemeMode.light, ), From 76d98de4d2d180eb8d050b7ad39c1f63ead38849 Mon Sep 17 00:00:00 2001 From: chan9an Date: Sun, 1 Mar 2026 15:07:48 +0530 Subject: [PATCH 2/5] refactor: address CodeRabbitAI review feedback for lifecycle, bounds checking, and encapsulation --- .../TaskWarriorWidgetProvider.kt | 19 +++++++--- .../home/controllers/home_controller.dart | 18 +++++----- .../splash/controllers/splash_controller.dart | 14 ++++---- lib/app/services/deep_link_service.dart | 35 +++++++++++++------ lib/main.dart | 11 ++++-- 5 files changed, 63 insertions(+), 34 deletions(-) diff --git a/android/app/src/main/kotlin/com/ccextractor/taskwarriorflutter/TaskWarriorWidgetProvider.kt b/android/app/src/main/kotlin/com/ccextractor/taskwarriorflutter/TaskWarriorWidgetProvider.kt index 157413c8..5eeb70ec 100644 --- a/android/app/src/main/kotlin/com/ccextractor/taskwarriorflutter/TaskWarriorWidgetProvider.kt +++ b/android/app/src/main/kotlin/com/ccextractor/taskwarriorflutter/TaskWarriorWidgetProvider.kt @@ -122,10 +122,10 @@ class ListViewRemoteViewsFactory( private val tasks = mutableListOf() - override fun onCreate() {} + override fun onCreate() = Unit override fun onDataSetChanged() { - tasks.clear() // Add this! + val newTasks = mutableListOf() val sharedPrefs = HomeWidgetPlugin.getData(context) val latestTasksJson = sharedPrefs.getString("tasks", "") @@ -133,15 +133,18 @@ class ListViewRemoteViewsFactory( try { val jsonArray = OrgJSONArray(latestTasksJson) for (i in 0 until jsonArray.length()) { - tasks.add(Task.fromJson(jsonArray.getJSONObject(i))) + newTasks.add(Task.fromJson(jsonArray.getJSONObject(i))) } } catch (e: JSONException) { e.printStackTrace() } } + // Atomic swap + tasks.clear() + tasks.addAll(newTasks) } - override fun onDestroy() {} + override fun onDestroy() = Unit override fun getCount(): Int = tasks.size @@ -168,7 +171,6 @@ class ListViewRemoteViewsFactory( return layoutId } fun getDotIdByPriority(p: String): Int { - println("PRIORITY: " + p) if (p.equals("L")) return R.drawable.low_priority_dot if (p.equals("M")) return R.drawable.mid_priority_dot if (p.equals("H")) return R.drawable.high_priority_dot @@ -176,6 +178,13 @@ class ListViewRemoteViewsFactory( } override fun getViewAt(position: Int): RemoteViews { + // Safe guard against Android out-of-bounds scrolling crashes + if (position !in tasks.indices) { + return RemoteViews(context.packageName, getListItemLayoutIdForR1()).apply { + setTextViewText(R.id.tv, "Loading...") + } + } + val task = tasks[position] if (task.uuid.equals("NO_TASK")) return RemoteViews(context.packageName, getListItemLayoutIdForR1()).apply { diff --git a/lib/app/modules/home/controllers/home_controller.dart b/lib/app/modules/home/controllers/home_controller.dart index e0723672..3cf342f8 100644 --- a/lib/app/modules/home/controllers/home_controller.dart +++ b/lib/app/modules/home/controllers/home_controller.dart @@ -129,17 +129,15 @@ class HomeController extends GetxController { @override void onReady() { super.onReady(); - // Automatically check for any queued Deep Links when Home spins up. - // We delay slightly to ensure the Navigator route swap finishes first, avoiding widget tree lock. - Future.delayed(const Duration(milliseconds: 50), () { + // Replaced 50ms delay with a secure PostFrameCallback + WidgetsBinding.instance.addPostFrameCallback((_) { if (isClosed) return; - if (Get.isRegistered()) { - final deepLinkService = Get.find(); - if (deepLinkService.queuedUri != null) { - debugPrint( - "TRACE: HomeController.onReady() consuming deferred queue!"); - deepLinkService.consumePendingActions(this); - } + + final deepLinkService = Get.find(); + if (deepLinkService.queuedUri != null) { + debugPrint( + "🚀 TRACE: HomeController.onReady() consuming deferred queue!"); + deepLinkService.consumePendingActions(this); } }); } diff --git a/lib/app/modules/splash/controllers/splash_controller.dart b/lib/app/modules/splash/controllers/splash_controller.dart index b1e3742d..a126ef0e 100644 --- a/lib/app/modules/splash/controllers/splash_controller.dart +++ b/lib/app/modules/splash/controllers/splash_controller.dart @@ -23,25 +23,27 @@ class SplashController extends GetxController { Profiles get _profiles => Profiles(baseDirectory.value); @override - void onInit() async { + void onInit() { debugPrint("🚀 BOOT: SplashController.onInit()"); super.onInit(); + } + + @override + void onReady() async { + super.onReady(); - // If we don't, HomeController will boot blind and crash trying to read empty paths. await initBaseDir(); _checkProfiles(); profilesMap.value = _profiles.profilesMap(); currentProfile.value = _profiles.getCurrentProfile()!; - // FIX 2: NOW we check if we should bypass the slow UI stuff. final deepLinkService = Get.find(); if (deepLinkService.queuedUri != null) { debugPrint("🚀 TRACE: Bypassing Splash routing for queued URI"); Get.offNamed(Routes.HOME); - return; // Skip the slow app updates and onboarding checks + return; } - // Normal boot sequence for people just opening the app normally await checkForUpdate(); sendToNextPage(); } @@ -173,4 +175,4 @@ class SplashController extends GetxController { debugPrint(e.toString()); } } -} \ No newline at end of file +} diff --git a/lib/app/services/deep_link_service.dart b/lib/app/services/deep_link_service.dart index 5d72fa13..5db40922 100644 --- a/lib/app/services/deep_link_service.dart +++ b/lib/app/services/deep_link_service.dart @@ -1,3 +1,4 @@ +import 'dart:async'; // Add this import at the top import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:app_links/app_links.dart'; @@ -7,7 +8,9 @@ import 'package:taskwarrior/app/routes/app_pages.dart'; class DeepLinkService extends GetxService { late AppLinks _appLinks; - String? queuedUri; + String? _queuedUri; // Made private + String? get queuedUri => _queuedUri; // Added getter + StreamSubscription? _linkSubscription; // Added stream subscription Future init() async { _appLinks = AppLinks(); @@ -15,33 +18,45 @@ class DeepLinkService extends GetxService { try { final initialUri = await _appLinks.getInitialLink(); if (initialUri != null) { - queuedUri = initialUri.toString(); - debugPrint('🔗 INITIAL LINK QUEUED: $queuedUri'); + _queuedUri = initialUri.toString(); + debugPrint('🔗 INITIAL LINK QUEUED: $_queuedUri'); } } catch (e) { - debugPrint('Deep link init error (safe to ignore on unsupported platforms): $e'); + debugPrint('Deep link init error: $e'); } - _appLinks.uriLinkStream.listen((uri) { + _linkSubscription = _appLinks.uriLinkStream.listen((uri) { debugPrint('🔗 LINK RECEIVED: $uri'); _handleWidgetUri(uri); + }, onError: (err) { + debugPrint('🔗 LINK STREAM ERROR: $err'); }); } + @override + void onClose() { + _linkSubscription?.cancel(); + super.onClose(); + } + void _handleWidgetUri(Uri uri) { if (Get.isRegistered()) { _executeAction(uri, Get.find()); } else { debugPrint("⏳ HomeController not ready. Queuing action."); - queuedUri = uri.toString(); + _queuedUri = uri.toString(); } } void consumePendingActions(HomeController controller) { - if (queuedUri != null) { + if (_queuedUri != null) { debugPrint("🚀 Executing queued action..."); - _executeAction(Uri.parse(queuedUri!), controller); - queuedUri = null; + try { + _executeAction(Uri.parse(_queuedUri!), controller); + } catch (e) { + debugPrint("🔗 FAILED TO PARSE URI: $_queuedUri - Error: $e"); + } + _queuedUri = null; } } @@ -71,4 +86,4 @@ class DeepLinkService extends GetxService { } } } -} \ No newline at end of file +} diff --git a/lib/main.dart b/lib/main.dart index 9f00e179..16812cf3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -27,9 +27,9 @@ DynamicLibrary loadNativeLibrary() { } void main() async { - debugPrint("🚀 BOOT: main() started"); WidgetsFlutterBinding.ensureInitialized(); + // Move the logger override ABOVE the first boot print! debugPrint = (String? message, {int? wrapWidth}) { if (message != null) { debugPrintSynchronously(message, wrapWidth: wrapWidth); @@ -37,6 +37,8 @@ void main() async { } }; + debugPrint("🚀 BOOT: main() started"); + loadNativeLibrary(); await RustLib.init(); @@ -45,7 +47,7 @@ void main() async { // fix: Actually await the service initialization so the OS intent is caught BEFORE runApp. await Get.putAsync(() async { final service = DeepLinkService(); - await service.init(); + await service.init(); return service; }, permanent: true); runApp( @@ -56,7 +58,10 @@ void main() async { initialRoute: AppPages.INITIAL, unknownRoute: AppPages.routes.firstWhere( (page) => page.name == AppPages.INITIAL, - orElse: () => AppPages.routes.first, + orElse: () { + debugPrint("⚠️ Unknown route requested, falling back to default"); + return AppPages.routes.first; + }, ), getPages: AppPages.routes, themeMode: AppSettings.isDarkMode ? ThemeMode.dark : ThemeMode.light, From 8b81c0cea28099d6c0fc7c584000274c943a4c88 Mon Sep 17 00:00:00 2001 From: chan9an Date: Tue, 3 Mar 2026 22:18:34 +0530 Subject: [PATCH 3/5] fix:properly configure deep-link routing context --- android/app/src/main/AndroidManifest.xml | 4 ++++ lib/app/services/deep_link_service.dart | 18 ++++++++++-------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index b6ffe5bd..ec226a5c 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -28,6 +28,10 @@ android:name="io.flutter.embedding.android.NormalTheme" android:resource="@style/NormalTheme" /> + diff --git a/lib/app/services/deep_link_service.dart b/lib/app/services/deep_link_service.dart index 5db40922..c80df04c 100644 --- a/lib/app/services/deep_link_service.dart +++ b/lib/app/services/deep_link_service.dart @@ -74,15 +74,17 @@ class DeepLinkService extends GetxService { } } else if (uri.host == "addclicked") { if (Get.context != null) { - Get.dialog( - Material( - child: AddTaskBottomSheet( - homeController: controller, - forTaskC: isTaskChampion, - forReplica: isReplica, + WidgetsBinding.instance.addPostFrameCallback((_) { + Get.dialog( + Material( + child: AddTaskBottomSheet( + homeController: controller, + forTaskC: isTaskChampion, + forReplica: isReplica, + ), ), - ), - ); + ); + }); } } } From 08cbad68d02d838e780beb78114b95a4e93c50c3 Mon Sep 17 00:00:00 2001 From: chan9an Date: Sun, 8 Mar 2026 11:04:31 +0530 Subject: [PATCH 4/5] feat: implement native app shortcuts --- android/app/src/main/res/drawable/plus.png | Bin 0 -> 8789 bytes android/app/src/main/res/drawable/report.png | Bin 0 -> 9572 bytes .../plus.imageset/Contents.json | 21 ++++ .../Assets.xcassets/plus.imageset/plus.png | Bin 0 -> 8789 bytes .../report.imageset/Contents.json | 21 ++++ .../report.imageset/report.png | Bin 0 -> 9572 bytes lib/app/services/deep_link_service.dart | 65 +++++++++++-- lib/main.dart | 8 +- pubspec.lock | 92 +++++++++++++++++- pubspec.yaml | 1 + 10 files changed, 196 insertions(+), 12 deletions(-) create mode 100755 android/app/src/main/res/drawable/plus.png create mode 100755 android/app/src/main/res/drawable/report.png create mode 100644 ios/Runner/Assets.xcassets/plus.imageset/Contents.json create mode 100755 ios/Runner/Assets.xcassets/plus.imageset/plus.png create mode 100644 ios/Runner/Assets.xcassets/report.imageset/Contents.json create mode 100755 ios/Runner/Assets.xcassets/report.imageset/report.png diff --git a/android/app/src/main/res/drawable/plus.png b/android/app/src/main/res/drawable/plus.png new file mode 100755 index 0000000000000000000000000000000000000000..de5cf864aeb4b4ef30a84c33f7ee09c8ec6537d3 GIT binary patch literal 8789 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable/report.png b/android/app/src/main/res/drawable/report.png new file mode 100755 index 0000000000000000000000000000000000000000..495314712a0ce8ad211c2315129738d8ca227c5e GIT binary patch literal 9572 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 literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/plus.imageset/Contents.json b/ios/Runner/Assets.xcassets/plus.imageset/Contents.json new file mode 100644 index 00000000..849cb5db --- /dev/null +++ b/ios/Runner/Assets.xcassets/plus.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "plus.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/plus.imageset/plus.png b/ios/Runner/Assets.xcassets/plus.imageset/plus.png new file mode 100755 index 0000000000000000000000000000000000000000..de5cf864aeb4b4ef30a84c33f7ee09c8ec6537d3 GIT binary patch literal 8789 zcmeHt`#)6Q_y0a~)nIgy`?wB8ZbislZgW!9P!uxdg;X+?P>PvUDuqTal}j%MDMcYd z9Pf%=U6hGP$?4*iTTHJaxqkNa`g|{c!uR`le14e6oH=Lhb=KZ{?X{lkx#rA9Pj^|G z3Jm}ti?UqU01)6M0Z38c*N@naDfqR3%i4Mn0Qv*T2WgGG_6NMF9Odd4_& z2#AS^F%I9i=V0&-ZisQ@fv_v1wkiN<0@TIHCpPD6@8OGqA-SX9A~)-uB1tW9A_cEa zGhKds(I4!Kf4XkH*}I&kZBnn-hV(J@=>TM)r*#|5~^9MlY?v_x?mg(bA0z zg_jz5+Gk}}5&bv9bPDZU#Zv~@#6)&E=HU&7kx-u_KyFebMz}#F3&pd z2YMncxSACtE6$G>Nx;VCl)z_&DoYoS#uYIm>^U~3?OYn5z>B`LPUT%HDMe+rA#9g6 zx?w^UuWL^o=KJq2zPj2FJiluv*2Z^5Y41zTbq+Y(H?eW{Ef&55%@NZ)KJaj*{&+Y*Y7Jl2f$V5IlNwA03V8X?D{j$Fr*P$|LZbOYHIezN4OWvLY{vcUs!IH1tah|lu?zLuy>M)r7b&2`OQ$nCyB3NA zL`%1-vLbsdlJu^x%mQ^b*-@2eb=#eo=&^g}eJK0R(%J5fg}tvQuvY$+{o*X4o{dy( zUeyU3Gb@j;Rz$In3BjH^qRMa)_*`2mymI@PgEcGAikLPcGow0a$fhQsyYo+C{g@)F z>3iiD8KCA_r#N0PpeYcpo1s||YxS&I!kltK_PU-&W68p&Gfebwx3bleZe&vH(suo{ zVHs>Ku8m)l4A^Liln^}%6Mc+Y4}$mW>&zcE2MMBHb`P$ z$k;B9B`zA!Oz*5Bn92lzoF#DQgqxC(xHMzu$Y;0s8w`Lm7RLW=c2$Nhb`*cx?sTM6 zND!xD^@YwXLB}I4Y_F*3cy-rBg4ls4=L+8zPB76l@(p6-1h3t&9E_3Y?5$5QQ9Emj zA2mRg@rUgp6EH?eV$BTwI!E^O$cS>^Oj1ccKoYs0AXYyHi<@u1Sb{)r2` zMVMTH2exZ5QG+*?e0TnEVPeOhC)KC{p2wb?W}+cIq#PHO>HoMW@_u1Pv7&sYzDWYe zlPfz-TX>j`MPrumOfdVyd__vN$9#B?!+%P%yx|M_cgcy0_*pIuu|7zwnYjIGS0=O6 zI5RC1iQhIZ?VdxdkeXazx&p7(MXz6}#=i54HaG6-`~udKja#5s3ynerEiK`admbQx znzQBGo^=ABm^i`}{*4<)nhml*ir%r{?PDgW=&l8^mEchJ@zHeyU_-1SNE;lnLj{+2 zk-4T~)*BO_&iZfP`HH?jwh5T4JK@Ih`;4bgUe88i7jQ#d%XLxqtMed%5U}a*N91YGhbC)mVgSTZZERR ziF(!WpN!3*m@qGAB@}-05L`^m7rfsXxqJ!>xo?x8!3{-QGnx8|UOK(jlPv{u0WC6^ zExu1wJuhH}o5~?C2wRpOez__Xv=LL5f2fr#o{h*Zs?qziQ*s_+rBtizHIvRPWnE## z$TZx%MpQUvi5-+Id+9E3R`A{(XAMC)lAC{Vzm8#^e1tXM(>b_+^ZHT7lt0v~p#RvHbn6e{X!mT`XO#0=R3>GfY zCLdvfch1`0?o;OFr|p~3K?OT|3APD$yoSOkc&xvw45VK-LrkKFtAJMNf)$OjU4s#7MkT^?C-q?#dC<#hekz+{ym^LcNt%^_SxURXGDB4B?KlO>yd{$OSQ`HKs%F{adIrFA z-b2u_{*y8BTn)*TF==O7|MK?;If57QbP z_3j&7g?;6B7Ut`&?z6)-LxdEFtrNYN>}hYxM|;G3w8DRlZ4_O4@cjnepj19p5Z}^u zes*UGLCnVQ7%WB&p3`97+KG?ar`}ewVptLVHm)~Bol>Z`)TU2*dH|C$y=39yca#Ps zRzbXIN@GUrxx$R>AgRC?Sjy&jvM)xoOn8#b za`8d@)=Z!CQ0!(FJbQA8&6dUP>_v^VhQuG0{wLpHVS^u{3kAD_We3B4p0T9*zW{3I z5}P{YhxwJfu1&qTTzJAaZ+_+Aoz_}m&n2>&`GmB;BCqhqQ|)k&DtO)RpqTyPs7Uug zDk(IrqrSwDxHk}_l$o9u&Xpl4MB`LCVTKpg|FY+d`f~jovg7WGaqKTFwIK1_ejTwv z06lH!)*hsr(OubND9{_s#g?{fSx>5q^9B|QTE=BKHAeF*?*iMJDfslm@WWfbygnZl zwKxlghS&Jdncf9z9Uu6Dp%3lljR;rDAD#II_D>_nl`fu5}v{eafBmq2smmQz8>yucdD%#}Y_{?hS2p!& zAyJ;QH430_v&qktTLu$KZ>guyH}drM1{wD6YBn9Tzyw2K8=`oGst<8#J-gnUnl7_G zDNK3o4&PZf00h>T&19aP_tZiK2CKW20V?>%td*dNvf~2?X-s`*V}vER_~)7jSX;Y3 zboruaWlE?^2@G8smvgKt4$c@WO6p`iwU$<^ zTr2Ijpp5dGp!#tqs`ApDqi0I>lNym$qcxDSHL^WcbCAbf{~hIUL&5c$f8*RjRo6v^ z;MYCbNoF|SC!gcWZrFh{@p811_~ZmxQ{yL+fF9`@Yq}@&^hD;AyX~w`KM6w~Kk((p z06ACc1)sP|{tH6+d3wQt!%H3OZ&KR`+mjNTgzs&oa%edlrWgg82PjwXtCP2r1JoHV z>|loUOc1AJK+_w?XW~(Uegz zahhJxe2?@}eGKLr_Pu`t&m%77DkEOTR%6~sHzN{aI`O*!O_g(I;H!8gOxBx&PyDcP zm;EB+`28lBKrA2zzj*$?1IbK-cv+O;|~vcTo9PvGIc6MZNOjK3hf+*{*9O}n@{fx0@IeD%%fS-PhF zo;mt%Q)$Oaqg2U{rv=1o2@1T>gTq1Sg&9T%2VVQDG91~jU%OB@&Kv$d)KHIEA(8c@ zr)&TWOkJ>&zdaBGf3 zsr?QW8e$GC{BEvRt(ERJvOZ0LYaaKEXndNkRoZEvn3%8!?q|O|{uPd^7OjbWv;Rgd z>7~6Y&lhAtr|VE@Mf%9a?$k~Dp=VSnSRu75txpSEYzUL;re@ZsCGKJKoOBC#o`b&C z4)b4Yb5jNst=176*)DTrC$u5PGOOdwLarW{N}t=xrD!@h5bcownZ2|&ZTcAwG!gU zf3s!%fN)CXEPdm`gjUw-&kJ@)qPHXgocixQ|DVf$egFUMbvVV}AgNB-MHaHe;X-c+ zT-V*K1Tc5!A9?`uygY=??BU$s8T)ri*UFM4>z7BQA;$l;)l$Y*K6fpI!b8nFIESKW z5T2KIKESU3JMKo#?{PPb;6n7b{?6>57xR3AUrDfs8^j)ro9dL&R8Ota!mTjPzdSYO zw7Zacik|^-!J(>y5POWk%+GdTk;JF+s@qX!MDQ4dH-7RGIPluEjs&<~e=J5BMiSs+ zaa4}|){xrG23uPC(>fjr{x=X>2Z2!8>8Dzy$)5^{@_09lZ+CAKplw5Y?VsnZbpYKf zApVLdBZUEBosFF}1}^6W?yf9(2Hq`Fa$(1Wi9f#xPha0m(CqAFh(DWT*7I9G-eC>x zM7Rf{PTS`YY|SL&ZE1#y%cL>A9CF&wP!rL)T}y9=)Fd}b3T5=t_ns>8G??ooJIxa--|60b0jykRWjAE}Yft+6F#@hhvxv5;OrnQTl zJ#j?g{{YeZxr7Eg>xWit$aY1>T2XC$W51k#zTb~MoEN(*S(i_YWBG)>Wcr@6SlLH17|mM5}Sv z=%)*h)JKmbz;hnjvuIM`_7_qPF@W}r_Dr)hrYm(e_2vsCn`Y}W)vsNu+(^lhwe@Qs z{z}X7vew_kHnUwj^`^I~UmDg0dr9#=A=)T0&PeQm+g*(7gG9-C(XBt}bL`4@kF%RK ztmp4=l^u8I1@aH_{n5l97YV6$>+Al(^>7cN-2&$7rxw9+C9{vbuEu96p_gH$>YT!TcL7@qc@FlLKivF;7zfp+I5lsv`dmW-*+ zjqj9HuxHQNoJy@Bfx%H;6FGW?VG1OW-V%j_^XGfjP1|KKH{9XudL}AZ_fxh-dFIa= z|D-w@7-%ZjTSa?SkwCaI-i3`4TQ5M0Y1(Lzo_D1sZlj4AP~}S}vwjbA)l-7QlmTm6 z$|U#UCrH$cyYY?aDQ|9B?K~Z@4O^cdYwu8CtNPwp)@i9pRmtc26+)Pf*N@DW_XF7 z84+-)pWgXHhT>%H2~=jz^cMk$|FX}6bi@<0!Q1!xN@x6eU&en0Kt(xM_|D6wh4G;P z*mr)B@DI1`+h3$lk^+mBsqbKLyWO6t!v~-w*z@fytR4!Ep0lSWC>?dqXP}j!UM^M*6?D}1 zU3fq1@Olg>mQmsm!@qubh@?XMj{MZZPdU*yKFn-jq|xO z)5H7JR-bfq zl2Ams_frX!gynO08~cc!%O|r1%muedDJDZ4S4AF?@6RtKYxoL;IS~Xp62GMbN!~{2 z{uqr*lQp~r!gt3q3Dkt;@e~00*L|F<#VJN8d!ITpaH8h=<3xr1%J}O!mbKG|B3(6q zD1CHcZQVd(=1L+MRO6}gb#)n#qfmQCQggDVUfme76!5+Xj(e^6wiyUU=7yx&v`-l_zzYam^^djMup{htsS0kis{8y;l<-S)CB&uCQls~mUz$#Q*^?LA|pn>XH6eLWN0 z&87u7_jEW4n7UmT-fQ|=cyB+1fbJ_*rOlp5Ib4%o+x6C);$1ULwso!n!A8{g3~?PK zp4!*=*iJfEe*x#ly*XYDzC5OWnp*OJhZ|#>X~9X^(FB)Ta};8Vm>@ zX0iDc`lciT;Mzf0Ir5t@9$0}<3!EKipivRj07UIKxx9ErG5oI0XJOOL&QbuLX+5X> z?t};lMmk@QPdi4X84Ya#KmoDDGZ4rz`E5{{$#SDAub496d1fNZh^Z1zKHM>tSbjA$ z(V?RffB@-+1xjaNgMpoIK@1aQ-rZAT?2APGb5OMHFI3W%xhz~HQlD3JqYf6gkpZqJ z3KU2nDJ0g9N?Dm$Ka3sP4m7`J*q)5a{{u35qlH*&f%AId0w`<1aet@dDDa8=f@q}B z+mnms0qY9st7qNe<4p9>_ejj9!1>2@6iTu=avT(*X+cXZh*g!XJ90Ce%ppzEO_x}$ zvVtKOIN|f9DXGweCp`{eEScV(TC4zg!#oGUI!8v0c_y>EOG=!D z^|?|9ebc3Zus36rxIIM|8ghOSZin4#ccQfafNO2WwGo$KRj`s2M*F+>y#FZ;pL7<- zJWnF_wbVfkazZ|AB zm=a&UEO)ZYI>Lr}HC+)2TGs3C2wOyD+V{^itg>cZIor@W=YuZdv~TP`p=A3nhRd8rp# z&KiEcL#n6_6NHjdB=ew+>)=tMkk4EH3a4<&lK9Ci%O%#Qj?O!<*taYQ^Q0Un>p>Hn zdu{Lx5JY=1dx<+_)Fwq8?yGKh(*D(^7*9O9Ae*Xoyoe>}*-e_X4R@F?YTo*vHQjcd z(r6feR1wbJ=UNE%mcODPvE!r}@ETu%2cdhM-9OH2pu6kNV_`gRUj6aPjVoP*0;rDB|J&3#k6}Q$1=NH<4O&u^*#4&%f+6eUsu%! z7BWjuWy1PA9P^ye0x&4({TH?@L73{8CZ>F#}j#tGK6I z6j>?%+@2=f=f+!szM9D*g@xNmJ62B++X!u>ze(_&b$;XkR-N*d>2cqOgoVM@gLR#i ztgc&AW19MwI@mZQa}m?+g%PQMO7P$%@)_3mPk{oOwoa@;2(3c zA8AC+P&3!Jg8M8#&@HX!%OcVFLtD=MY9Q1S{V01=XvInj;LxfhAIjgzLHGH%r>*bW zQ*(J=d0*Z0E6u`962pZy+UpEnZxd}Dzrs&)Z<n$lmUH~S0MnMTE&u=k literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/report.imageset/Contents.json b/ios/Runner/Assets.xcassets/report.imageset/Contents.json new file mode 100644 index 00000000..a8fbdcf3 --- /dev/null +++ b/ios/Runner/Assets.xcassets/report.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images": [ + { + "idiom": "universal", + "filename": "report.png", + "scale": "1x" + }, + { + "idiom": "universal", + "scale": "2x" + }, + { + "idiom": "universal", + "scale": "3x" + } + ], + "info": { + "version": 1, + "author": "xcode" + } +} \ No newline at end of file diff --git a/ios/Runner/Assets.xcassets/report.imageset/report.png b/ios/Runner/Assets.xcassets/report.imageset/report.png new file mode 100755 index 0000000000000000000000000000000000000000..495314712a0ce8ad211c2315129738d8ca227c5e GIT binary patch literal 9572 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 literal 0 HcmV?d00001 diff --git a/lib/app/services/deep_link_service.dart b/lib/app/services/deep_link_service.dart index f016c93c..29357919 100644 --- a/lib/app/services/deep_link_service.dart +++ b/lib/app/services/deep_link_service.dart @@ -1,6 +1,8 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:app_links/app_links.dart'; +import 'package:quick_actions/quick_actions.dart'; import 'package:taskwarrior/app/modules/home/views/add_task_bottom_sheet_new.dart'; import 'package:taskwarrior/app/modules/home/controllers/home_controller.dart'; import 'package:taskwarrior/app/routes/app_pages.dart'; @@ -8,21 +10,64 @@ import 'package:taskwarrior/app/routes/app_pages.dart'; class DeepLinkService extends GetxService { late AppLinks _appLinks; Uri? _queuedUri; + Uri? get queuedUri => _queuedUri; - @override - void onReady() { - super.onReady(); - _initDeepLinks(); - } + final QuickActions _quickActions = const QuickActions(); + StreamSubscription? _linkSubscription; - void _initDeepLinks() { + Future init() async { _appLinks = AppLinks(); - _appLinks.uriLinkStream.listen((uri) { + + await _initQuickActions(); + + try { + final initialUri = await _appLinks.getInitialLink(); + if (initialUri != null) { + _queuedUri = initialUri; + debugPrint('🔗 INITIAL LINK QUEUED: $_queuedUri'); + } + } catch (e) { + debugPrint('Deep link init error: $e'); + } + + _linkSubscription = _appLinks.uriLinkStream.listen((uri) { debugPrint('🔗 LINK RECEIVED: $uri'); _handleWidgetUri(uri); + }, onError: (err) { + debugPrint('🔗 LINK STREAM ERROR: $err'); }); } + Future _initQuickActions() async { + await _quickActions.initialize((String shortcutType) { + debugPrint("⚡ SHORTCUT RECEIVED: $shortcutType"); + if (shortcutType == 'shortcut_add_task') { + _handleWidgetUri(Uri.parse('taskwarrior://addclicked')); + } else if (shortcutType == 'shortcut_reports') { + _handleWidgetUri(Uri.parse('taskwarrior://reports')); + } + }); + + await _quickActions.setShortcutItems([ + const ShortcutItem( + type: 'shortcut_add_task', + localizedTitle: 'Add Task', + icon: 'plus', + ), + const ShortcutItem( + type: 'shortcut_reports', + localizedTitle: 'Reports', + icon: 'report', + ), + ]); + } + + @override + void onClose() { + _linkSubscription?.cancel(); + super.onClose(); + } + void _handleWidgetUri(Uri uri) { if (Get.isRegistered()) { _executeAction(uri, Get.find()); @@ -64,6 +109,12 @@ class DeepLinkService extends GetxService { ), ); } + } else if (uri.host == "reports") { + if (Get.context != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + Get.toNamed(Routes.REPORTS); + }); + } } } } diff --git a/lib/main.dart b/lib/main.dart index 09f0268c..d21785af 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,8 +3,6 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -// 1. Add this import -import 'package:app_links/app_links.dart'; import 'package:taskwarrior/app/services/deep_link_service.dart'; import 'package:taskwarrior/app/utils/app_settings/app_settings.dart'; @@ -42,7 +40,11 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); await AppSettings.init(); - Get.put(DeepLinkService(), permanent: true); + await Get.putAsync(() async { + final service = DeepLinkService(); + await service.init(); + return service; + }, permanent: true); runApp( GetMaterialApp( darkTheme: darkTheme, diff --git a/pubspec.lock b/pubspec.lock index c7182030..c8475ca7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -25,6 +25,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + app_links: + dependency: "direct main" + description: + name: app_links + sha256: "5f88447519add627fe1cbcab4fd1da3d4fed15b9baf29f28b22535c95ecee3e8" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + app_links_linux: + dependency: transitive + description: + name: app_links_linux + sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + app_links_platform_interface: + dependency: transitive + description: + name: app_links_platform_interface + sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + app_links_web: + dependency: transitive + description: + name: app_links_web + sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 + url: "https://pub.dev" + source: hosted + version: "1.0.4" archive: dependency: transitive description: @@ -65,6 +97,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0" + build_cli_annotations: + dependency: transitive + description: + name: build_cli_annotations + sha256: e563c2e01de8974566a1998410d3f6f03521788160a02503b0b1f1a46c7b3d95 + url: "https://pub.dev" + source: hosted + version: "2.1.1" build_config: dependency: transitive description: @@ -519,6 +559,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.32" + flutter_rust_bridge: + dependency: "direct main" + description: + name: flutter_rust_bridge + sha256: "37ef40bc6f863652e865f0b2563ea07f0d3c58d8efad803cc01933a4b2ee067e" + url: "https://pub.dev" + source: hosted + version: "2.11.1" flutter_slidable: dependency: "direct main" description: @@ -601,6 +649,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + gtk: + dependency: transitive + description: + name: gtk + sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + url: "https://pub.dev" + source: hosted + version: "2.1.0" hashcodes: dependency: transitive description: @@ -1049,6 +1105,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" + quick_actions: + dependency: "direct main" + description: + name: quick_actions + sha256: "7e35dd6a21f5bbd21acf6899039eaf85001a5ac26d52cbd6a8a2814505b90798" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + quick_actions_android: + dependency: transitive + description: + name: quick_actions_android + sha256: "23f04632ada7fc16665d84ba54a0c792c09727e7fda6c989c6e6ba1853aa15dc" + url: "https://pub.dev" + source: hosted + version: "1.0.27" + quick_actions_ios: + dependency: transitive + description: + name: quick_actions_ios + sha256: a2e08ceb01f9d26e1b1826b1c4f5da6b7b6bbf61bcbaacd8e93dfff58b91f996 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + quick_actions_platform_interface: + dependency: transitive + description: + name: quick_actions_platform_interface + sha256: "1fec7068db5122cd019e9340d3d7be5d36eab099695ef3402c7059ee058329a4" + url: "https://pub.dev" + source: hosted + version: "1.1.0" quiver: dependency: transitive description: @@ -1141,10 +1229,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "3.0.0" sizer: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 12e80726..e1cf0ae4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -69,6 +69,7 @@ dependencies: flutter_rust_bridge: ^2.11.1 ffi: any # Required for FFI app_links: ^6.4.1 + quick_actions: ^1.1.0 dev_dependencies: build_runner: null From 49d5853e0b14dc54138086d68b6fb92bd7987742 Mon Sep 17 00:00:00 2001 From: chan9an Date: Sun, 15 Mar 2026 15:14:11 +0530 Subject: [PATCH 5/5] fix: resolve GetX bottom sheet keyboard insets and background widget task sync --- .../src/main/res/drawable/ic_shortcut_add.png | Bin 0 -> 119 bytes .../main/res/drawable/ic_shortcut_reports.png | Bin 0 -> 267 bytes .../ic_shortcut_add.imageset/Contents.json | 23 ++++ .../ic_shortcut_add.imageset/add_1x.png | Bin 0 -> 109 bytes .../ic_shortcut_add.imageset/add_2x.png | Bin 0 -> 119 bytes .../ic_shortcut_add.imageset/add_3x.png | Bin 0 -> 119 bytes .../Contents.json | 23 ++++ .../reports_1x.png | Bin 0 -> 162 bytes .../reports_2x.png | Bin 0 -> 267 bytes .../reports_3x.png | Bin 0 -> 267 bytes .../controllers/detail_route_controller.dart | 8 ++ .../home/views/add_task_bottom_sheet_new.dart | 98 ++++++++--------- .../home_page_floating_action_button.dart | 44 ++++---- lib/app/modules/home/views/show_tasks.dart | 14 +++ .../home/views/show_tasks_replica.dart | 12 ++ lib/app/modules/home/views/tasks_builder.dart | 6 + lib/app/services/deep_link_service.dart | 103 +++++++++++++++--- pubspec.lock | 92 +++++++++++++++- pubspec.yaml | 1 + 19 files changed, 334 insertions(+), 90 deletions(-) create mode 100644 android/app/src/main/res/drawable/ic_shortcut_add.png create mode 100644 android/app/src/main/res/drawable/ic_shortcut_reports.png create mode 100644 ios/Runner/Assets.xcassets/ic_shortcut_add.imageset/Contents.json create mode 100644 ios/Runner/Assets.xcassets/ic_shortcut_add.imageset/add_1x.png create mode 100644 ios/Runner/Assets.xcassets/ic_shortcut_add.imageset/add_2x.png create mode 100644 ios/Runner/Assets.xcassets/ic_shortcut_add.imageset/add_3x.png create mode 100644 ios/Runner/Assets.xcassets/ic_shortcut_reports.imageset/Contents.json create mode 100644 ios/Runner/Assets.xcassets/ic_shortcut_reports.imageset/reports_1x.png create mode 100644 ios/Runner/Assets.xcassets/ic_shortcut_reports.imageset/reports_2x.png create mode 100644 ios/Runner/Assets.xcassets/ic_shortcut_reports.imageset/reports_3x.png diff --git a/android/app/src/main/res/drawable/ic_shortcut_add.png b/android/app/src/main/res/drawable/ic_shortcut_add.png new file mode 100644 index 0000000000000000000000000000000000000000..3cb10924a0912d9f64d338b9769da053bc051da6 GIT binary patch literal 119 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeK3?y%aJ*@^(YymzYu0R?HmZtAK52P4Ng8YIR z9G=}s19Id&T^vIy7?T^C0uTIeOgX2~{@-4rW`SaRO?zLF8zVzTLHf)E76O$Z{hqFV JF6*2UngE2qAnO1C literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable/ic_shortcut_reports.png b/android/app/src/main/res/drawable/ic_shortcut_reports.png new file mode 100644 index 0000000000000000000000000000000000000000..ffa30f7f04ea414d9d5f4f21f61bd9ade0ca65b8 GIT binary patch literal 267 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeE3?v1%WpM*3l>na*S0GIgEQ?@r2P)zz3GxeO z(CWMT=|AhFKf5MgVG%Ig1r+b{ba4!+U`$$ICc(Pc;e?6^e?gc_`22fPUE(fN7OqO4 zE#<}FqP8I80hcO=*n*4+3TqY$x;ASq_-kdt#L$wcVI|95uwcgIzZU}EvaV~BZ(eX- z-S7od8PBu^v1Y{twIDWzPn--E4t$Ia%N^nw9=JQq)?|0PXu4`k;I&@M1fISpOA6g4 e+n;{te1g$q>Wj0>Uo5!^@{*^kpUXO@geCy95Kcw_ literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/ic_shortcut_add.imageset/Contents.json b/ios/Runner/Assets.xcassets/ic_shortcut_add.imageset/Contents.json new file mode 100644 index 00000000..f520ef98 --- /dev/null +++ b/ios/Runner/Assets.xcassets/ic_shortcut_add.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images": [ + { + "idiom": "universal", + "filename": "add_1x.png", + "scale": "1x" + }, + { + "idiom": "universal", + "filename": "add_2x.png", + "scale": "2x" + }, + { + "idiom": "universal", + "filename": "add_3x.png", + "scale": "3x" + } + ], + "info": { + "version": 1, + "author": "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/ic_shortcut_add.imageset/add_1x.png b/ios/Runner/Assets.xcassets/ic_shortcut_add.imageset/add_1x.png new file mode 100644 index 0000000000000000000000000000000000000000..e00d6d14ba1be5eabdb2ca7649373865447e9c11 GIT binary patch literal 109 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}a}t6Hgb%kch)?FCFA$aNuBe{GA_V zAAV3#$znFsv&kHu*H4)P70qDyRNP>9z?YHXK$^p)b6lo1=M5tc+_eEpF?hQAxvX-t_ literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/ic_shortcut_add.imageset/add_2x.png b/ios/Runner/Assets.xcassets/ic_shortcut_add.imageset/add_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..3cb10924a0912d9f64d338b9769da053bc051da6 GIT binary patch literal 119 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeK3?y%aJ*@^(YymzYu0R?HmZtAK52P4Ng8YIR z9G=}s19Id&T^vIy7?T^C0uTIeOgX2~{@-4rW`SaRO?zLF8zVzTLHf)E76O$Z{hqFV JF6*2UngE2qAnO1C literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/ic_shortcut_add.imageset/add_3x.png b/ios/Runner/Assets.xcassets/ic_shortcut_add.imageset/add_3x.png new file mode 100644 index 0000000000000000000000000000000000000000..3cb10924a0912d9f64d338b9769da053bc051da6 GIT binary patch literal 119 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeK3?y%aJ*@^(YymzYu0R?HmZtAK52P4Ng8YIR z9G=}s19Id&T^vIy7?T^C0uTIeOgX2~{@-4rW`SaRO?zLF8zVzTLHf)E76O$Z{hqFV JF6*2UngE2qAnO1C literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/ic_shortcut_reports.imageset/Contents.json b/ios/Runner/Assets.xcassets/ic_shortcut_reports.imageset/Contents.json new file mode 100644 index 00000000..fcfa4f1f --- /dev/null +++ b/ios/Runner/Assets.xcassets/ic_shortcut_reports.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images": [ + { + "idiom": "universal", + "filename": "reports_1x.png", + "scale": "1x" + }, + { + "idiom": "universal", + "filename": "reports_2x.png", + "scale": "2x" + }, + { + "idiom": "universal", + "filename": "reports_3x.png", + "scale": "3x" + } + ], + "info": { + "version": 1, + "author": "xcode" + } +} \ No newline at end of file diff --git a/ios/Runner/Assets.xcassets/ic_shortcut_reports.imageset/reports_1x.png b/ios/Runner/Assets.xcassets/ic_shortcut_reports.imageset/reports_1x.png new file mode 100644 index 0000000000000000000000000000000000000000..cf16e965e0f898a9980264777c8847dfea6788dd GIT binary patch literal 162 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0DOivfbkch)?uNw+A7;v~;-0Obs z{KZLHLNPT+1GxOHIy0+8jXIdYMyS_VS8(S6Eh3gIK zybiJ-zQ{5h*ebxFAiv``JHrR|hOJMl7u5XJjlX}If5Io>t_dD|2h&3?o>{^=2WTmS Mr>mdKI;Vst0JPFR`Tzg` literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/ic_shortcut_reports.imageset/reports_2x.png b/ios/Runner/Assets.xcassets/ic_shortcut_reports.imageset/reports_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..ffa30f7f04ea414d9d5f4f21f61bd9ade0ca65b8 GIT binary patch literal 267 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeE3?v1%WpM*3l>na*S0GIgEQ?@r2P)zz3GxeO z(CWMT=|AhFKf5MgVG%Ig1r+b{ba4!+U`$$ICc(Pc;e?6^e?gc_`22fPUE(fN7OqO4 zE#<}FqP8I80hcO=*n*4+3TqY$x;ASq_-kdt#L$wcVI|95uwcgIzZU}EvaV~BZ(eX- z-S7od8PBu^v1Y{twIDWzPn--E4t$Ia%N^nw9=JQq)?|0PXu4`k;I&@M1fISpOA6g4 e+n;{te1g$q>Wj0>Uo5!^@{*^kpUXO@geCy95Kcw_ literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/ic_shortcut_reports.imageset/reports_3x.png b/ios/Runner/Assets.xcassets/ic_shortcut_reports.imageset/reports_3x.png new file mode 100644 index 0000000000000000000000000000000000000000..ffa30f7f04ea414d9d5f4f21f61bd9ade0ca65b8 GIT binary patch literal 267 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeE3?v1%WpM*3l>na*S0GIgEQ?@r2P)zz3GxeO z(CWMT=|AhFKf5MgVG%Ig1r+b{ba4!+U`$$ICc(Pc;e?6^e?gc_`22fPUE(fN7OqO4 zE#<}FqP8I80hcO=*n*4+3TqY$x;ASq_-kdt#L$wcVI|95uwcgIzZU}EvaV~BZ(eX- z-S7od8PBu^v1Y{twIDWzPn--E4t$Ia%N^nw9=JQq)?|0PXu4`k;I&@M1fISpOA6g4 e+n;{te1g$q>Wj0>Uo5!^@{*^kpUXO@geCy95Kcw_ literal 0 HcmV?d00001 diff --git a/lib/app/modules/detailRoute/controllers/detail_route_controller.dart b/lib/app/modules/detailRoute/controllers/detail_route_controller.dart index 8b65bc1c..833fa691 100644 --- a/lib/app/modules/detailRoute/controllers/detail_route_controller.dart +++ b/lib/app/modules/detailRoute/controllers/detail_route_controller.dart @@ -1,8 +1,10 @@ // ignore_for_file: depend_on_referenced_packages +import 'dart:io'; import 'package:built_collection/built_collection.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:taskwarrior/app/modules/home/controllers/widget.controller.dart'; import 'package:taskwarrior/app/modules/home/controllers/home_controller.dart'; import 'package:taskwarrior/app/tour/details_page_tour.dart'; import 'package:taskwarrior/app/utils/constants/taskwarrior_colors.dart'; @@ -92,6 +94,12 @@ class DetailRouteController extends GetxController { modify.save(modified: () => now); onEdit.value = false; + if (Platform.isAndroid || Platform.isIOS) { + WidgetController widgetController = Get.put(WidgetController()); + widgetController.fetchAllData(); + widgetController.update(); + } + // Show snackbar Get.snackbar( 'Task Updated', diff --git a/lib/app/modules/home/views/add_task_bottom_sheet_new.dart b/lib/app/modules/home/views/add_task_bottom_sheet_new.dart index 3442246f..395f2561 100644 --- a/lib/app/modules/home/views/add_task_bottom_sheet_new.dart +++ b/lib/app/modules/home/views/add_task_bottom_sheet_new.dart @@ -34,15 +34,16 @@ class AddTaskBottomSheet extends StatelessWidget { debugPrint( "Building Add Task Bottom Sheet for ${forTaskC ? "TaskC" : forReplica ? "Replica" : "Normal Task"}"); const padding = 12.0; - return Padding( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom, - ), - child: Form( - key: homeController.formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ + return SingleChildScrollView( + child: Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: Form( + key: homeController.formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ Padding( padding: const EdgeInsets.all(padding), child: Row( @@ -89,55 +90,48 @@ class AddTaskBottomSheet extends StatelessWidget { ], ), ), - Flexible( - child: SingleChildScrollView( - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(padding), - child: TextFormField( - controller: homeController.namecontroller, - validator: (value) => value!.trim().isEmpty - ? SentenceManager( - currentLanguage: - homeController.selectedLanguage.value) - .sentences - .descriprtionCannotBeEmpty - : null, - decoration: InputDecoration( - labelText: SentenceManager( - currentLanguage: - homeController.selectedLanguage.value) - .sentences - .enterTaskDescription, - border: OutlineInputBorder(), - ), - ), - ), - Padding( - padding: const EdgeInsets.all(padding), - child: buildProjectInput(context)), - Padding( - padding: const EdgeInsets.only( - left: padding, right: padding, top: padding), - child: buildDatePicker(context), - ), - Padding( - padding: const EdgeInsets.all(padding), - child: buildPriority(context), - ), - Padding( - padding: const EdgeInsets.all(padding), - child: buildTagsInput(context), - ), - const Padding(padding: EdgeInsets.all(20)), - ], + Padding( + padding: const EdgeInsets.all(padding), + child: TextFormField( + controller: homeController.namecontroller, + validator: (value) => value!.trim().isEmpty + ? SentenceManager( + currentLanguage: + homeController.selectedLanguage.value) + .sentences + .descriprtionCannotBeEmpty + : null, + decoration: InputDecoration( + labelText: SentenceManager( + currentLanguage: + homeController.selectedLanguage.value) + .sentences + .enterTaskDescription, + border: OutlineInputBorder(), ), ), ), + Padding( + padding: const EdgeInsets.all(padding), + child: buildProjectInput(context)), + Padding( + padding: const EdgeInsets.only( + left: padding, right: padding, top: padding), + child: buildDatePicker(context), + ), + Padding( + padding: const EdgeInsets.all(padding), + child: buildPriority(context), + ), + Padding( + padding: const EdgeInsets.all(padding), + child: buildTagsInput(context), + ), + const Padding(padding: EdgeInsets.all(20)), ], ), ), + ), ); } diff --git a/lib/app/modules/home/views/home_page_floating_action_button.dart b/lib/app/modules/home/views/home_page_floating_action_button.dart index 96107c46..384f5f77 100644 --- a/lib/app/modules/home/views/home_page_floating_action_button.dart +++ b/lib/app/modules/home/views/home_page_floating_action_button.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:taskwarrior/app/modules/home/views/add_task_bottom_sheet_new.dart'; import 'package:taskwarrior/app/utils/themes/theme_extension.dart'; @@ -27,28 +28,28 @@ class HomePageFloatingActionButton extends StatelessWidget { color: tColors.secondaryBackgroundColor, ), ), - onPressed: () => (showModalBottomSheet( - backgroundColor: tColors.dialogBackgroundColor, - context: context, - isScrollControlled: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(0), - topRight: Radius.circular(0), - ), + onPressed: () { + Get.bottomSheet( + AddTaskBottomSheet( + homeController: controller, + forTaskC: controller.taskchampion.value, + forReplica: controller.taskReplica.value, + ), + backgroundColor: tColors.dialogBackgroundColor, + isScrollControlled: true, + ignoreSafeArea: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(0), + topRight: Radius.circular(0), ), - builder: (context) => AddTaskBottomSheet( - homeController: controller, - forTaskC: controller.taskchampion.value, - forReplica: controller.taskReplica.value, - ), - ).then((value) { - if (controller.isSyncNeeded.value && value != "cancel") { - controller.isNeededtoSyncOnStart(context); - } - })) - - // .then((value) { + ), + ).then((value) { + if (controller.isSyncNeeded.value && value != "cancel") { + controller.isNeededtoSyncOnStart(context); + } + }); + }, // .then((value) { // // print(value); // //if auto sync is turned on @@ -65,3 +66,4 @@ class HomePageFloatingActionButton extends StatelessWidget { ); } } + diff --git a/lib/app/modules/home/views/show_tasks.dart b/lib/app/modules/home/views/show_tasks.dart index 008b30fb..48cc5de4 100644 --- a/lib/app/modules/home/views/show_tasks.dart +++ b/lib/app/modules/home/views/show_tasks.dart @@ -1,5 +1,7 @@ +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; +import 'package:taskwarrior/app/modules/home/controllers/widget.controller.dart'; import 'package:get/get.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:taskwarrior/app/modules/home/controllers/home_controller.dart'; @@ -231,6 +233,12 @@ class TaskViewBuilder extends StatelessWidget { await taskDatabase.open(); taskDatabase.markTaskAsCompleted(uuid); completeTask('email', uuid); + + if (Platform.isAndroid || Platform.isIOS) { + WidgetController widgetController = Get.put(WidgetController()); + widgetController.fetchAllData(); + widgetController.update(); + } } void _markTaskAsDeleted(String uuid) async { @@ -238,6 +246,12 @@ class TaskViewBuilder extends StatelessWidget { await taskDatabase.open(); taskDatabase.markTaskAsDeleted(uuid); deleteTask('email', uuid); + + if (Platform.isAndroid || Platform.isIOS) { + WidgetController widgetController = Get.put(WidgetController()); + widgetController.fetchAllData(); + widgetController.update(); + } } Color _getPriorityColor(String priority) { diff --git a/lib/app/modules/home/views/show_tasks_replica.dart b/lib/app/modules/home/views/show_tasks_replica.dart index 642fbdff..fb406546 100644 --- a/lib/app/modules/home/views/show_tasks_replica.dart +++ b/lib/app/modules/home/views/show_tasks_replica.dart @@ -1,4 +1,6 @@ +import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:taskwarrior/app/modules/home/controllers/widget.controller.dart'; import 'package:get/get.dart'; import 'package:taskwarrior/app/modules/home/controllers/home_controller.dart'; import 'package:google_fonts/google_fonts.dart'; @@ -222,10 +224,20 @@ class TaskReplicaViewBuilder extends StatelessWidget { void completeTask(TaskForReplica task) async { await Replica.modifyTaskInReplica(task.copyWith(status: 'completed')); Get.find().refreshReplicaTaskList(); + if (Platform.isAndroid || Platform.isIOS) { + WidgetController widgetController = Get.put(WidgetController()); + widgetController.fetchAllData(); + widgetController.update(); + } } void deleteTask(TaskForReplica task) async { await Replica.deleteTaskFromReplica(task.uuid); Get.find().refreshReplicaTaskList(); + if (Platform.isAndroid || Platform.isIOS) { + WidgetController widgetController = Get.put(WidgetController()); + widgetController.fetchAllData(); + widgetController.update(); + } } } diff --git a/lib/app/modules/home/views/tasks_builder.dart b/lib/app/modules/home/views/tasks_builder.dart index 54846126..187dc6ad 100644 --- a/lib/app/modules/home/views/tasks_builder.dart +++ b/lib/app/modules/home/views/tasks_builder.dart @@ -197,6 +197,12 @@ class TasksBuilder extends StatelessWidget { dtb!.add(const Duration(minutes: 1)); cancelNotification(task); } + if (Platform.isAndroid || Platform.isIOS) { + WidgetController widgetController = + Get.put(WidgetController()); + widgetController.fetchAllData(); + widgetController.update(); + } }, icon: Icons.done, label: SentenceManager( diff --git a/lib/app/services/deep_link_service.dart b/lib/app/services/deep_link_service.dart index c80df04c..09619d20 100644 --- a/lib/app/services/deep_link_service.dart +++ b/lib/app/services/deep_link_service.dart @@ -1,20 +1,29 @@ -import 'dart:async'; // Add this import at the top +import 'dart:async'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:app_links/app_links.dart'; +import 'package:quick_actions/quick_actions.dart'; import 'package:taskwarrior/app/modules/home/views/add_task_bottom_sheet_new.dart'; import 'package:taskwarrior/app/modules/home/controllers/home_controller.dart'; +import 'package:taskwarrior/app/modules/detailRoute/controllers/detail_route_controller.dart'; import 'package:taskwarrior/app/routes/app_pages.dart'; +import 'package:taskwarrior/app/utils/themes/theme_extension.dart'; class DeepLinkService extends GetxService { late AppLinks _appLinks; - String? _queuedUri; // Made private - String? get queuedUri => _queuedUri; // Added getter - StreamSubscription? _linkSubscription; // Added stream subscription + String? _queuedUri; + String? get queuedUri => _queuedUri; + StreamSubscription? _linkSubscription; + final QuickActions _quickActions = const QuickActions(); Future init() async { _appLinks = AppLinks(); + if (Platform.isAndroid || Platform.isIOS) { + await _initQuickActions(); + } + try { final initialUri = await _appLinks.getInitialLink(); if (initialUri != null) { @@ -33,6 +42,31 @@ class DeepLinkService extends GetxService { }); } + Future _initQuickActions() async { + await _quickActions.initialize((String shortcutType) { + debugPrint("⚡ SHORTCUT RECEIVED: $shortcutType"); + if (shortcutType == 'shortcut_add_task') { + _handleWidgetUri(Uri.parse('taskwarrior://shortcut_add')); + } else if (shortcutType == 'shortcut_reports') { + _handleWidgetUri(Uri.parse('taskwarrior://reports')); + } + }); + + await _quickActions.setShortcutItems([ + const ShortcutItem( + type: 'shortcut_add_task', + localizedTitle: 'Add Task', + icon: 'ic_shortcut_add', // We will provide these via Material HD assets + ), + const ShortcutItem( + type: 'shortcut_reports', + localizedTitle: 'Reports', + icon: + 'ic_shortcut_reports', // We will provide these via Material HD assets + ), + ]); + } + @override void onClose() { _linkSubscription?.cancel(); @@ -40,10 +74,10 @@ class DeepLinkService extends GetxService { } void _handleWidgetUri(Uri uri) { - if (Get.isRegistered()) { + if (Get.isRegistered() && Get.context != null) { _executeAction(uri, Get.find()); } else { - debugPrint("⏳ HomeController not ready. Queuing action."); + debugPrint("⏳ HomeController or UI not ready. Queuing action."); _queuedUri = uri.toString(); } } @@ -70,22 +104,61 @@ class DeepLinkService extends GetxService { !isTaskChampion && !isReplica) { String uuid = uri.queryParameters["uuid"] as String; - Get.toNamed(Routes.DETAIL_ROUTE, arguments: ["uuid", uuid]); + + // Android Deep Links must cleanly present the new intent data. + // Forcibly clear the previous task controller from GetX singleton memory + // so that the new route triggers onInit() and loads the new task's UUID. + if (Get.isRegistered()) { + Get.delete(force: true); + } + + // Pop all active pages, dialogs, and sheets until we hit the persistent Home route, + // then push the new task route. This guarantees a clean hierarchy and a fresh UI rebuild. + Get.offNamedUntil( + Routes.DETAIL_ROUTE, + (route) => route.settings.name == Routes.HOME, + arguments: ["uuid", uuid], + ); } - } else if (uri.host == "addclicked") { + } else if (uri.host == "addclicked" || uri.host == "shortcut_add") { if (Get.context != null) { WidgetsBinding.instance.addPostFrameCallback((_) { - Get.dialog( - Material( - child: AddTaskBottomSheet( - homeController: controller, - forTaskC: isTaskChampion, - forReplica: isReplica, + // If the bottom sheet is already open, doing nothing prevents async stacking issues! + // Get.back() animating at the exact same time as a new Get.bottomSheet() causes + // Get.isBottomSheetOpen to desync inside GetX state management on the third instance. + if (Get.isBottomSheetOpen ?? false) { + return; + } + + TaskwarriorColorTheme tColors = + Theme.of(Get.context!).extension()!; + + Get.bottomSheet( + AddTaskBottomSheet( + homeController: controller, + forTaskC: isTaskChampion, + forReplica: isReplica, + ), + backgroundColor: tColors.dialogBackgroundColor, + isScrollControlled: true, + ignoreSafeArea: true, // TRUE allows MediaQuery.viewInsets (software keyboard) to reach inner Padding + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(0), + topRight: Radius.circular(0), ), ), - ); + ).then((value) { + if (controller.isSyncNeeded.value && value != "cancel") { + controller.isNeededtoSyncOnStart(Get.context!); + } + }); }); } + } else if (uri.host == "reports") { + WidgetsBinding.instance.addPostFrameCallback((_) { + Get.toNamed(Routes.REPORTS); + }); } } } diff --git a/pubspec.lock b/pubspec.lock index c7182030..c8475ca7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -25,6 +25,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + app_links: + dependency: "direct main" + description: + name: app_links + sha256: "5f88447519add627fe1cbcab4fd1da3d4fed15b9baf29f28b22535c95ecee3e8" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + app_links_linux: + dependency: transitive + description: + name: app_links_linux + sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + app_links_platform_interface: + dependency: transitive + description: + name: app_links_platform_interface + sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + app_links_web: + dependency: transitive + description: + name: app_links_web + sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 + url: "https://pub.dev" + source: hosted + version: "1.0.4" archive: dependency: transitive description: @@ -65,6 +97,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0" + build_cli_annotations: + dependency: transitive + description: + name: build_cli_annotations + sha256: e563c2e01de8974566a1998410d3f6f03521788160a02503b0b1f1a46c7b3d95 + url: "https://pub.dev" + source: hosted + version: "2.1.1" build_config: dependency: transitive description: @@ -519,6 +559,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.32" + flutter_rust_bridge: + dependency: "direct main" + description: + name: flutter_rust_bridge + sha256: "37ef40bc6f863652e865f0b2563ea07f0d3c58d8efad803cc01933a4b2ee067e" + url: "https://pub.dev" + source: hosted + version: "2.11.1" flutter_slidable: dependency: "direct main" description: @@ -601,6 +649,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + gtk: + dependency: transitive + description: + name: gtk + sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + url: "https://pub.dev" + source: hosted + version: "2.1.0" hashcodes: dependency: transitive description: @@ -1049,6 +1105,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" + quick_actions: + dependency: "direct main" + description: + name: quick_actions + sha256: "7e35dd6a21f5bbd21acf6899039eaf85001a5ac26d52cbd6a8a2814505b90798" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + quick_actions_android: + dependency: transitive + description: + name: quick_actions_android + sha256: "23f04632ada7fc16665d84ba54a0c792c09727e7fda6c989c6e6ba1853aa15dc" + url: "https://pub.dev" + source: hosted + version: "1.0.27" + quick_actions_ios: + dependency: transitive + description: + name: quick_actions_ios + sha256: a2e08ceb01f9d26e1b1826b1c4f5da6b7b6bbf61bcbaacd8e93dfff58b91f996 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + quick_actions_platform_interface: + dependency: transitive + description: + name: quick_actions_platform_interface + sha256: "1fec7068db5122cd019e9340d3d7be5d36eab099695ef3402c7059ee058329a4" + url: "https://pub.dev" + source: hosted + version: "1.1.0" quiver: dependency: transitive description: @@ -1141,10 +1229,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "3.0.0" sizer: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 12e80726..e1cf0ae4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -69,6 +69,7 @@ dependencies: flutter_rust_bridge: ^2.11.1 ffi: any # Required for FFI app_links: ^6.4.1 + quick_actions: ^1.1.0 dev_dependencies: build_runner: null