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