diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 6be7f747..b8f116a9 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -36,6 +36,7 @@ + @@ -44,6 +45,22 @@ + + + + + + + + + + + + + + if (call.method == "updateWidget") { + updateWidget() + result.success("Widget updated") + } else { + result.notImplemented() + } + } + } + + private fun updateWidget() { + val intent = Intent(this, WidgetUpdateReceiver::class.java).apply { + action = "UPDATE_WIDGET" + } + sendBroadcast(intent) + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/ccextractor/taskwarriorflutter/WidgetUpdateReceiver.kt b/android/app/src/main/kotlin/com/ccextractor/taskwarriorflutter/WidgetUpdateReceiver.kt new file mode 100644 index 00000000..49d9375c --- /dev/null +++ b/android/app/src/main/kotlin/com/ccextractor/taskwarriorflutter/WidgetUpdateReceiver.kt @@ -0,0 +1,68 @@ +package com.ccextractor.taskwarriorflutter + +import android.appwidget.AppWidgetManager +import android.view.View +import android.content.BroadcastReceiver +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.graphics.BitmapFactory +import android.util.Log +import android.widget.RemoteViews +import java.io.File +import com.ccextractor.taskwarriorflutter.R +import es.antonborri.home_widget.HomeWidgetPlugin + +class WidgetUpdateReceiver : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == "UPDATE_WIDGET") { + Log.d("WidgetUpdateReceiver", "Received UPDATE_WIDGET broadcast") + + val appWidgetManager = AppWidgetManager.getInstance(context) + val appWidgetIds = appWidgetManager.getAppWidgetIds(ComponentName(context, BurndownChartProvider::class.java)) + + for (appWidgetId in appWidgetIds) { + updateAppWidget(context, appWidgetManager, appWidgetId) + } + } + } + + private fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int) { + Log.d("WidgetUpdateReceiver", "Updating widget $appWidgetId") + + val views = RemoteViews(context.packageName, R.layout.report_layout) + + // Retrieve the chart image path from HomeWidget + val chartImage = HomeWidgetPlugin.getData(context).getString("chart_image", null) + + if (chartImage != null) { + Log.d("WidgetUpdateReceiver", "Chart image path: $chartImage") + val file = File(chartImage) + if (file.exists()) { + Log.d("WidgetUpdateReceiver", "File exists!") + val b = BitmapFactory.decodeFile(file.absolutePath) + if (b != null) { + Log.d("WidgetUpdateReceiver", "Bitmap decoded successfully!") + views.setImageViewBitmap(R.id.widget_image, b) + views.setViewVisibility(R.id.widget_image, View.VISIBLE) + views.setViewVisibility(R.id.no_image_text, View.GONE) + } else { + Log.e("WidgetUpdateReceiver", "Bitmap decoding failed!") + views.setViewVisibility(R.id.widget_image, View.GONE) + views.setViewVisibility(R.id.no_image_text, View.VISIBLE) + } + } else { + Log.e("WidgetUpdateReceiver", "File does not exist: $chartImage") + views.setViewVisibility(R.id.widget_image, View.GONE) + views.setViewVisibility(R.id.no_image_text, View.VISIBLE) + } + } else { + Log.d("WidgetUpdateReceiver", "No chart image saved yet") + views.setViewVisibility(R.id.widget_image, View.GONE) + views.setViewVisibility(R.id.no_image_text, View.VISIBLE) + } + + appWidgetManager.updateAppWidget(appWidgetId, views) + } +} \ No newline at end of file diff --git a/android/app/src/main/res/drawable/preview_report.jpg b/android/app/src/main/res/drawable/preview_report.jpg new file mode 100644 index 00000000..761c8ff8 Binary files /dev/null and b/android/app/src/main/res/drawable/preview_report.jpg differ diff --git a/android/app/src/main/res/layout/report_layout.xml b/android/app/src/main/res/layout/report_layout.xml new file mode 100644 index 00000000..cd6aaca4 --- /dev/null +++ b/android/app/src/main/res/layout/report_layout.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index ff9b9745..182c4b81 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -9,6 +9,8 @@ taskwarriorappwidget://cardclicked taskwarriorappwidget://addclicked This widget shows pending tasks from TaskWarrior app + This widget shows the daily reports graph to track your progress + Click Refresh button in Daily Reports Tab TaskWarrior Add widget Task List diff --git a/android/app/src/main/res/xml/burndownchartconfig.xml b/android/app/src/main/res/xml/burndownchartconfig.xml new file mode 100644 index 00000000..fff84148 --- /dev/null +++ b/android/app/src/main/res/xml/burndownchartconfig.xml @@ -0,0 +1,10 @@ + + \ No newline at end of file diff --git a/lib/app/modules/reports/controllers/reports_controller.dart b/lib/app/modules/reports/controllers/reports_controller.dart index e33e2d66..17ab277f 100644 --- a/lib/app/modules/reports/controllers/reports_controller.dart +++ b/lib/app/modules/reports/controllers/reports_controller.dart @@ -1,9 +1,10 @@ -// ignore_for_file: prefer_typing_uninitialized_variables - import 'dart:io'; - +import 'package:home_widget/home_widget.dart'; +import 'dart:ui'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:syncfusion_flutter_charts/charts.dart'; import 'package:taskwarrior/api_service.dart'; import 'package:taskwarrior/app/models/json/task.dart'; @@ -16,7 +17,8 @@ import 'package:taskwarrior/app/utils/constants/taskwarrior_fonts.dart'; import 'package:taskwarrior/app/utils/constants/utilites.dart'; import 'package:taskwarrior/app/utils/gen/fonts.gen.dart'; import 'package:taskwarrior/app/utils/app_settings/app_settings.dart'; - +import 'package:path_provider/path_provider.dart'; +import 'package:flutter/services.dart'; import 'package:tutorial_coach_mark/tutorial_coach_mark.dart'; class ReportsController extends GetxController @@ -34,6 +36,69 @@ class ReportsController extends GetxController late Storage storage; var storageWidget; + final GlobalKey _chartKey = GlobalKey(); + + GlobalKey get chartKey => _chartKey; + + Future captureChart() async { + try { + if (chartKey.currentContext == null) { + print('Error: chartKey.currentContext is null'); + return; + } + + RenderRepaintBoundary? boundary = + chartKey.currentContext!.findRenderObject() as RenderRepaintBoundary?; + + if (boundary == null) { + print('Error: boundary is null'); + return; + } + + final image = await boundary.toImage(); + final byteData = await image.toByteData(format: ImageByteFormat.png); + + if (byteData == null) { + print('Error: byteData is null'); + return; + } + + final pngBytes = byteData.buffer.asUint8List(); + + // Get the documents directory + final directory = await getApplicationDocumentsDirectory(); + final imagePath = '${directory.path}/daily_burndown_chart.png'; + + // Save the image to the documents directory + File imgFile = File(imagePath); + await imgFile.writeAsBytes(pngBytes); + print('Image saved to: $imagePath'); + + // Save the image path to HomeWidget + await HomeWidget.saveWidgetData('chart_image', imagePath); + + // Verify that the file exists + if (await imgFile.exists()) { + print('Image file exists!'); + } else { + print('Image file does not exist!'); + } + + // Add a delay before sending the broadcast + await Future.delayed(const Duration(milliseconds: 500)); + + // Send a broadcast to update the widget + const platform = MethodChannel('com.example.taskwarrior/widget'); + try { + await platform.invokeMethod('updateWidget'); + } on PlatformException catch (e) { + print("Failed to Invoke: '${e.message}'."); + } + } catch (e) { + print('Error capturing chart: $e'); + } + } + // void _initReportsTour() { // tutorialCoachMark = TutorialCoachMark( // targets: reportsDrawer( diff --git a/lib/app/modules/reports/views/burn_down_daily.dart b/lib/app/modules/reports/views/burn_down_daily.dart index e57096a8..ff2c33c3 100644 --- a/lib/app/modules/reports/views/burn_down_daily.dart +++ b/lib/app/modules/reports/views/burn_down_daily.dart @@ -18,88 +18,122 @@ class BurnDownDaily extends StatelessWidget { Widget build(BuildContext context) { double height = MediaQuery.of(context).size.height; - return Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, + return Stack( children: [ - Expanded( - child: SizedBox( - height: height * 0.6, - child: Obx( - () => SfCartesianChart( - primaryXAxis: CategoryAxis( - title: AxisTitle( - text: SentenceManager( - currentLanguage: AppSettings.selectedLanguage) - .sentences - .reportsPageDailyDayMonth, - textStyle: TextStyle( - fontFamily: FontFamily.poppins, - fontWeight: TaskWarriorFonts.bold, - color: AppSettings.isDarkMode - ? Colors.white - : Colors.black, - fontSize: TaskWarriorFonts.fontSizeSmall, + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: SizedBox( + height: height * 0.6, + child: RepaintBoundary( + key: reportsController.chartKey, + child: Obx( + () => SfCartesianChart( + primaryXAxis: CategoryAxis( + title: AxisTitle( + text: SentenceManager( + currentLanguage: AppSettings.selectedLanguage) + .sentences + .reportsPageDailyDayMonth, + textStyle: TextStyle( + fontFamily: FontFamily.poppins, + fontWeight: TaskWarriorFonts.bold, + color: AppSettings.isDarkMode + ? Colors.white + : Colors.black, + fontSize: TaskWarriorFonts.fontSizeSmall, + ), + ), ), - ), - ), - primaryYAxis: NumericAxis( - title: AxisTitle( - text: SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.reportsPageTasks, - textStyle: TextStyle( - fontFamily: FontFamily.poppins, - fontWeight: TaskWarriorFonts.bold, - color: AppSettings.isDarkMode - ? Colors.white - : Colors.black, - fontSize: TaskWarriorFonts.fontSizeSmall, + primaryYAxis: NumericAxis( + title: AxisTitle( + text: SentenceManager( + currentLanguage: AppSettings.selectedLanguage) + .sentences + .reportsPageTasks, + textStyle: TextStyle( + fontFamily: FontFamily.poppins, + fontWeight: TaskWarriorFonts.bold, + color: AppSettings.isDarkMode + ? Colors.white + : Colors.black, + fontSize: TaskWarriorFonts.fontSizeSmall, + ), + ), ), - ), - ), - tooltipBehavior: - reportsController.dailyBurndownTooltipBehaviour, - series: [ - /// This is the completed tasks - StackedColumnSeries( - groupName: 'Group A', - enableTooltip: true, - color: TaskWarriorColors.green, - dataSource: reportsController.dailyInfo.entries - .map((entry) => ChartData( - entry.key, - entry.value['pending'] ?? 0, - entry.value['completed'] ?? 0, - )) - .toList(), - xValueMapper: (ChartData data, _) => data.x, - yValueMapper: (ChartData data, _) => data.y2, - name: 'Completed', - ), + tooltipBehavior: + reportsController.dailyBurndownTooltipBehaviour, + series: [ + /// This is the completed tasks + StackedColumnSeries( + groupName: 'Group A', + enableTooltip: true, + color: TaskWarriorColors.green, + dataSource: reportsController.dailyInfo.entries + .map((entry) => ChartData( + entry.key, + entry.value['pending'] ?? 0, + entry.value['completed'] ?? 0, + )) + .toList(), + xValueMapper: (ChartData data, _) => data.x, + yValueMapper: (ChartData data, _) => data.y2, + name: 'Completed', + ), - /// This is the pending tasks - StackedColumnSeries( - groupName: 'Group A', - color: TaskWarriorColors.yellow, - enableTooltip: true, - dataSource: reportsController.dailyInfo.entries - .map((entry) => ChartData( - entry.key, - entry.value['pending'] ?? 0, - entry.value['completed'] ?? 0, - )) - .toList(), - xValueMapper: (ChartData data, _) => data.x, - yValueMapper: (ChartData data, _) => data.y1, - name: 'Pending', + /// This is the pending tasks + StackedColumnSeries( + groupName: 'Group A', + color: TaskWarriorColors.yellow, + enableTooltip: true, + dataSource: reportsController.dailyInfo.entries + .map((entry) => ChartData( + entry.key, + entry.value['pending'] ?? 0, + entry.value['completed'] ?? 0, + )) + .toList(), + xValueMapper: (ChartData data, _) => data.x, + yValueMapper: (ChartData data, _) => data.y1, + name: 'Pending', + ), + ], ), - ], + ), ), - )), + ), + ), + CommonChartIndicator( + title: + SentenceManager(currentLanguage: AppSettings.selectedLanguage) + .sentences + .reportsPageDailyBurnDownChart, + ), + ], ), - CommonChartIndicator( - title: SentenceManager(currentLanguage: AppSettings.selectedLanguage) - .sentences - .reportsPageDailyBurnDownChart, + Positioned( + bottom: 16, + right: 16, + child: InkWell( + onTap: () { + WidgetsBinding.instance.addPostFrameCallback((_) { + reportsController.captureChart(); + }); + }, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.grey.withOpacity(0.3), // Adjust opacity as needed + borderRadius: BorderRadius.circular(8), + ), + child: const Icon( + Icons.refresh, + color: Colors.white, + ), + ), + ), ), ], ); diff --git a/pubspec.lock b/pubspec.lock index 90d1a52e..9a596b38 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -165,10 +165,10 @@ packages: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" color: dependency: transitive description: @@ -273,6 +273,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.10" + dio: + dependency: transitive + description: + name: dio + sha256: "7d328c4d898a61efc3cd93655a0955858e29a0aa647f0f9e02d59b3bb275e2e8" + url: "https://pub.dev" + source: hosted + version: "4.0.6" double_back_to_close_app: dependency: "direct main" description: @@ -616,6 +624,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + http_mock_adapter: + dependency: "direct dev" + description: + name: http_mock_adapter + sha256: "6fa1a00932a5758750f54c5594f8bf37393fe5ae86e49325281b5a9d99114f33" + url: "https://pub.dev" + source: hosted + version: "0.3.3" http_multi_server: dependency: transitive description: @@ -700,18 +716,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.7" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.8" leak_tracker_testing: dependency: transitive description: @@ -756,18 +772,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" mime: dependency: transitive description: @@ -841,21 +857,21 @@ packages: source: hosted version: "1.0.1" path_provider: - dependency: transitive + dependency: "direct main" description: name: path_provider - sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72 + sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.15" path_provider_foundation: dependency: transitive description: @@ -1105,7 +1121,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_gen: dependency: transitive description: @@ -1162,14 +1178,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.5.4" + sqflite_common_ffi: + dependency: "direct dev" + description: + name: sqflite_common_ffi + sha256: "4d6137c29e930d6e4a8ff373989dd9de7bac12e3bc87bce950f6e844e8ad3bb5" + url: "https://pub.dev" + source: hosted + version: "2.3.3" + sqlite3: + dependency: transitive + description: + name: sqlite3 + sha256: "1abbeb84bf2b1a10e5e1138c913123c8aa9d83cd64e5f9a0dd847b3c83063202" + url: "https://pub.dev" + source: hosted + version: "2.4.2" stack_trace: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.0" stream_channel: dependency: transitive description: @@ -1190,10 +1222,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" syncfusion_flutter_charts: dependency: "direct main" description: @@ -1230,26 +1262,26 @@ packages: dependency: "direct main" description: name: test - sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073" + sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f" url: "https://pub.dev" source: hosted - version: "1.25.2" + version: "1.25.8" test_api: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.3" test_core: dependency: transitive description: name: test_core - sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4" + sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.5" time: dependency: transitive description: @@ -1422,10 +1454,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.3.0" watcher: dependency: transitive description: @@ -1491,5 +1523,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.3.0 <4.0.0" - flutter: ">=3.20.0-1.2.pre" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/pubspec.yaml b/pubspec.yaml index 19db109c..41fbc734 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,6 +64,7 @@ dependencies: url_launcher: ^6.1.14 uuid: ^4.2.2 built_collection: ^5.1.1 + path_provider: ^2.1.5 dev_dependencies: build_runner: null