Skip to content
13 changes: 12 additions & 1 deletion lib/app/models/filters.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import 'package:taskwarrior/app/services/tag_filter.dart';
// lib/app/models/filters.dart

import 'package:taskwarrior/app/models/tag_filters.dart';


class Filters {
const Filters({
required this.pendingFilter,
required this.waitingFilter,
required this.completedFilter,
required this.deletedFilter,
required this.togglePendingFilter,
required this.toggleWaitingFilter,
required this.toggleCompletedFilter,
required this.toggleDeletedFilter,
required this.tagFilters,
required this.projects,
required this.projectFilter,
Expand All @@ -14,8 +21,12 @@ class Filters {

final bool pendingFilter;
final bool waitingFilter;
final bool completedFilter;
final bool deletedFilter;
final void Function() togglePendingFilter;
final void Function() toggleWaitingFilter;
final void Function() toggleCompletedFilter;
final void Function() toggleDeletedFilter;
final TagFilters tagFilters;
final dynamic projects;
final String projectFilter;
Expand Down
25 changes: 25 additions & 0 deletions lib/app/models/tag_filters.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// lib/app/models/tag_filters.dart

class TagFilterMetadata {
const TagFilterMetadata({
required this.display,
required this.selected,
});

final String display;
final bool selected;
}

class TagFilters {
const TagFilters({
required this.tagUnion,
required this.toggleTagUnion,
required this.tags,
required this.toggleTagFilter,
});

final bool tagUnion;
final void Function() toggleTagUnion;
final Map<String, TagFilterMetadata> tags;
final void Function(String) toggleTagFilter;
}
101 changes: 97 additions & 4 deletions lib/app/modules/home/controllers/home_controller.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
// ignore_for_file: use_build_context_synchronously, unrelated_type_equality_checks

import 'package:taskwarrior/app/utils/language/sentences.dart';

Comment on lines +3 to +4
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether `Sentences` is abstract/concrete and whether `TASKSERVER_SETUP` is declared.
rg -n --type=dart 'abstract class Sentences|class Sentences\b' lib/app/utils/language
rg -n --type=dart '\bTASKSERVER_SETUP\b' lib/app/routes

Repository: CCExtractor/taskwarrior-flutter

Length of output: 141


Fix compilation errors: Sentences is abstract and Routes.TASKSERVER_SETUP does not exist.

Line 54 instantiates Sentences() directly, but Sentences is an abstract class and cannot be instantiated. Line 612 references Routes.TASKSERVER_SETUP, which does not exist in the routes configuration. Both changes will fail to compile and must be corrected before merging.

Also applies to: 3-4, 8-8, 606-614

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/app/modules/home/controllers/home_controller.dart` around lines 3 - 4,
The code tries to instantiate the abstract Sentences class and references a
non-existent Routes.TASKSERVER_SETUP; change the Sentences() instantiation to
use the concrete implementation or factory (e.g., the project's concrete
subclass or a getSentences() provider) so you don't instantiate an abstract
class, and fix the route reference by replacing Routes.TASKSERVER_SETUP with the
actual route constant defined in your routes file (or add a TASKSERVER_SETUP
constant to the routes configuration if that route is intended to exist); update
all occurrences (the Sentences() construction and the Routes.TASKSERVER_SETUP
usages) to use the correct concrete class/factory and valid route constant.

import 'dart:collection';
import 'dart:io';

import 'package:taskwarrior/app/routes/app_pages.dart';

import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
Expand All @@ -18,7 +22,8 @@ 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';
import 'package:taskwarrior/app/services/deep_link_service.dart';
import 'package:taskwarrior/app/services/tag_filter.dart';
import 'package:taskwarrior/app/models/tag_filters.dart';

import 'package:taskwarrior/app/tour/filter_drawer_tour.dart';
import 'package:taskwarrior/app/tour/home_page_tour.dart';
import 'package:taskwarrior/app/tour/task_swipe_tour.dart';
Expand All @@ -40,12 +45,18 @@ import 'package:textfield_tags/textfield_tags.dart';
import 'package:taskwarrior/app/utils/themes/theme_extension.dart';
import 'package:tutorial_coach_mark/tutorial_coach_mark.dart';



class HomeController extends GetxController {
final SplashController splashController = Get.find<SplashController>();
late Storage storage;
final RxBool taskServerBannerShown = false.obs;
final Sentences sentences = Sentences();
final RxBool pendingFilter = false.obs;
final RxBool waitingFilter = false.obs;
final RxString projectFilter = ''.obs;
final RxBool completedFilter = false.obs;
final RxBool deletedFilter = false.obs;
final RxBool tagUnion = false.obs;
final RxString selectedSort = ''.obs;
final RxSet<String> selectedTags = <String>{}.obs;
Expand Down Expand Up @@ -100,6 +111,8 @@ class HomeController extends GetxController {
everAll([
pendingFilter,
waitingFilter,
completedFilter,
deletedFilter,
projectFilter,
tagUnion,
selectedSort,
Expand Down Expand Up @@ -244,15 +257,35 @@ class HomeController extends GetxController {
}

void _refreshTasks() {
if (pendingFilter.value) {

if (deletedFilter.value) {
// Show ONLY deleted tasks
queriedTasks.value = storage.data
.completedData()
.where((task) => task.status == 'deleted')
.toList();
}
else if (completedFilter.value) {
// Show completed tasks (EXCLUDE deleted)
queriedTasks.value = storage.data
.completedData()
.where((task) => task.status == 'completed')
.toList();
}
else if (pendingFilter.value) {
// Show pending tasks (default behaviour)
queriedTasks.value = storage.data
.pendingData()
.where((task) => task.status == 'pending')
.toList();
} else {
queriedTasks.value = storage.data.completedData();
}
else {
// Fallback: pending tasks
queriedTasks.value = storage.data.pendingData();
}


// Rest of the method stays the same...
if (waitingFilter.value) {
var currentTime = DateTime.now();
queriedTasks.value = queriedTasks
Expand Down Expand Up @@ -354,6 +387,22 @@ class HomeController extends GetxController {
_refreshTasks();
}

void toggleCompletedFilter() {
completedFilter.toggle();
if (completedFilter.value) {
deletedFilter.value = false;
}
_refreshTasks();
}

void toggleDeletedFilter() {
deletedFilter.toggle();
if (deletedFilter.value) {
completedFilter.value = false;
}
_refreshTasks();
}

void toggleProjectFilter(String project) {
Query(storage.tabs.tab()).toggleProjectFilter(project);
projectFilter.value = Query(storage.tabs.tab()).projectFilter();
Expand Down Expand Up @@ -544,6 +593,44 @@ class HomeController extends GetxController {
_refreshTasks();
}

void showTaskServerNotConfiguredBanner(BuildContext context) {
if (taskServerBannerShown.value) return;

taskServerBannerShown.value = true;
final messenger = ScaffoldMessenger.of(context);

messenger.clearMaterialBanners();

messenger.showMaterialBanner(
MaterialBanner(
content: Text(sentences.homePageTaskWarriorNotConfigured),
actions: [
TextButton(
onPressed: () {
messenger.hideCurrentMaterialBanner();
taskServerBannerShown.value = false; // ✅ RESET flag
Get.toNamed(Routes.TASKSERVER_SETUP),
},
child: Text(sentences.homePageSetup),
),
TextButton(
onPressed: () {
messenger.hideCurrentMaterialBanner();
taskServerBannerShown.value = false; // ✅ RESET flag
},
child: const Text('Dismiss'),
),
],
),
);

Future.delayed(const Duration(seconds: 5), () {
messenger.hideCurrentMaterialBanner();
taskServerBannerShown.value = false; // ✅ RESET flag
});
Comment on lines +596 to +630
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

The auto-dismiss task can clear the wrong banner.

Line 602 clears every material banner up front, and the delayed callback on Line 627 later calls hideCurrentMaterialBanner() on whichever banner is current at that moment. If this warning is dismissed or replaced before 5 seconds elapse, the timer can remove an unrelated banner, and the “shown” guard becomes reusable immediately after. Track the specific banner controller/timer instead of the global current banner.

🛠️ Safer banner lifecycle sketch
+import 'dart:async';
 import 'dart:collection';
 import 'dart:io';

 class HomeController extends GetxController {
+  Timer? _taskServerBannerTimer;
+  ScaffoldFeatureController<MaterialBanner, MaterialBannerClosedReason>?
+      _taskServerBannerController;
+
   void showTaskServerNotConfiguredBanner(BuildContext context) {
     if (taskServerBannerShown.value) return;

     taskServerBannerShown.value = true;
     final messenger = ScaffoldMessenger.of(context);

-    messenger.clearMaterialBanners();
-
-    messenger.showMaterialBanner(
+    _taskServerBannerTimer?.cancel();
+    _taskServerBannerController?.close();
+    _taskServerBannerController = messenger.showMaterialBanner(
       MaterialBanner(
         content: Text(sentences.homePageTaskWarriorNotConfigured),
         actions: [
           TextButton(
             onPressed: () {
-              messenger.hideCurrentMaterialBanner();
+              _taskServerBannerTimer?.cancel();
+              _taskServerBannerController?.close();
               taskServerBannerShown.value = false;
               Get.toNamed(Routes.TASKSERVER_SETUP);
             },
             child: Text(sentences.homePageSetup),
           ),
           TextButton(
             onPressed: () {
-              messenger.hideCurrentMaterialBanner();
+              _taskServerBannerTimer?.cancel();
+              _taskServerBannerController?.close();
               taskServerBannerShown.value = false;
             },
             child: const Text('Dismiss'),
           ),
         ],
       ),
     );

-    Future.delayed(const Duration(seconds: 5), () {
-      messenger.hideCurrentMaterialBanner();
+    _taskServerBannerTimer = Timer(const Duration(seconds: 5), () {
+      _taskServerBannerController?.close();
       taskServerBannerShown.value = false;
     });
   }

   `@override`
   void onClose() {
+    _taskServerBannerTimer?.cancel();
     searchFocusNode.dispose();
     super.onClose();
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/app/modules/home/controllers/home_controller.dart` around lines 596 -
630, The current implementation can dismiss the wrong banner because it clears
all banners and uses scaffold-wide hideCurrentMaterialBanner(); modify
showTaskServerNotConfiguredBanner to capture and track the specific
ScaffoldFeatureController returned by messenger.showMaterialBanner (e.g. store
it in a field like _taskServerBannerController) and use that controller.close()
to dismiss this banner; also create and store a Timer (e.g.
_taskServerBannerTimer) for the 5s auto-dismiss so you can cancel it when either
TextButton is pressed or before showing a new banner, remove the global
messenger.clearMaterialBanners() call, and ensure both button handlers and the
timer clear/reset taskServerBannerShown and null out the controller and timer so
the lifecycle is tied to this banner instance only.

}


void renameTab({
required String tab,
required String name,
Expand Down Expand Up @@ -656,11 +743,17 @@ class HomeController extends GetxController {
tags: tags,
toggleTagFilter: toggleTagFilter,
);

// REPLACE this entire Filters() instantiation:
var filters = Filters(
pendingFilter: pendingFilter.value,
waitingFilter: waitingFilter.value,
completedFilter: completedFilter.value, // NEW - Add this line
deletedFilter: deletedFilter.value, // NEW - Add this line
togglePendingFilter: togglePendingFilter,
toggleWaitingFilter: toggleWaitingFilter,
toggleCompletedFilter: toggleCompletedFilter, // NEW - Add this line
toggleDeletedFilter: toggleDeletedFilter, // NEW - Add this line
projects: projects,
projectFilter: projectFilter.value,
toggleProjectFilter: toggleProjectFilter,
Expand Down
66 changes: 35 additions & 31 deletions lib/app/modules/home/views/filter_drawer_home_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -97,17 +97,20 @@ class FilterDrawer extends StatelessWidget {
color: tColors.primaryTextColor,
)),
TextSpan(
text: filters.pendingFilter
? SentenceManager(
currentLanguage: homeController
.selectedLanguage.value)
.sentences
.filterDrawerPending
: SentenceManager(
currentLanguage: homeController
.selectedLanguage.value)
.sentences
.filterDrawerCompleted,
text: filters.deletedFilter
? 'Deleted'
: filters.completedFilter
? SentenceManager(
currentLanguage:
homeController.selectedLanguage.value)
.sentences
.filterDrawerCompleted
: SentenceManager(
currentLanguage:
homeController.selectedLanguage.value)
.sentences
.filterDrawerPending,

style: TextStyle(
fontFamily: FontFamily.poppins,
fontSize: TaskWarriorFonts.fontSizeMedium,
Expand All @@ -116,7 +119,16 @@ class FilterDrawer extends StatelessWidget {
],
),
),
onTap: filters.togglePendingFilter,
onTap: () {
if (filters.deletedFilter) {
filters.toggleDeletedFilter();
} else if (filters.completedFilter) {
filters.toggleCompletedFilter();
} else {
filters.togglePendingFilter();
}
},

textColor: tColors.primaryTextColor,
),
),
Expand All @@ -137,31 +149,23 @@ class FilterDrawer extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
!filters.waitingFilter
? SentenceManager(
currentLanguage: homeController
.selectedLanguage.value)
.sentences
.filterDrawerShowWaiting
: SentenceManager(
currentLanguage: homeController
.selectedLanguage.value)
.sentences
.filterDrawerHideWaiting,
style: TextStyle(
fontFamily: FontFamily.poppins,
fontSize: TaskWarriorFonts.fontSizeMedium,
color: tColors.primaryTextColor,
)),
'Show Deleted',
style: TextStyle(
fontFamily: FontFamily.poppins,
fontSize: TaskWarriorFonts.fontSizeMedium,
color: tColors.primaryTextColor,
),
),
Switch(
value: filters.waitingFilter,
onChanged: (_) => filters.toggleWaitingFilter(),
)
value: filters.deletedFilter,
onChanged: (_) => filters.toggleDeletedFilter(),
),
],
),
),
),
),

const Divider(
color: Color.fromARGB(0, 48, 46, 46),
),
Expand Down
Loading
Loading