diff --git a/lib/app/modules/home/views/home_page_nav_drawer_menu_item.dart b/lib/app/modules/home/views/home_page_nav_drawer_menu_item.dart index 435a54a6..e9aaa267 100644 --- a/lib/app/modules/home/views/home_page_nav_drawer_menu_item.dart +++ b/lib/app/modules/home/views/home_page_nav_drawer_menu_item.dart @@ -6,6 +6,8 @@ import 'package:taskwarrior/app/utils/themes/theme_extension.dart'; class NavDrawerMenuItem extends StatelessWidget { final IconData icon; final String text; + final Color? iconColor; + final Color? textColor; final VoidCallback onTap; const NavDrawerMenuItem({ @@ -13,32 +15,33 @@ class NavDrawerMenuItem extends StatelessWidget { required this.icon, required this.text, required this.onTap, + this.iconColor, + this.textColor, }); @override Widget build(BuildContext context) { - TaskwarriorColorTheme tColors = Theme.of(context).extension()!; - return InkWell( - onTap: onTap, - child: Container( - color: tColors.dialogBackgroundColor, - padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 10), - child: Row( - children: [ - Icon( - icon, - color: tColors.primaryTextColor, - ), - const SizedBox(width: 10), - Text( - text, - style: TextStyle( - color: tColors.primaryTextColor, - fontSize: TaskWarriorFonts.fontSizeMedium, - ), - ), - ], + TaskwarriorColorTheme tColors = + Theme.of(context).extension()!; + return Container( + margin: const EdgeInsets.only(bottom: 4), + child: ListTile( + onTap: onTap, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + leading: Icon( + icon, + color: iconColor, ), + title: Text( + text, + style: TextStyle( + color: textColor, + fontSize: TaskWarriorFonts.fontSizeMedium, + fontWeight: FontWeight.w500, + ), + ), + splashColor: tColors.primaryTextColor!.withOpacity(0.1), + hoverColor: tColors.primaryTextColor!.withOpacity(0.05), ), ); } diff --git a/lib/app/modules/home/views/nav_drawer.dart b/lib/app/modules/home/views/nav_drawer.dart index 82c0d074..9d430d8f 100644 --- a/lib/app/modules/home/views/nav_drawer.dart +++ b/lib/app/modules/home/views/nav_drawer.dart @@ -1,3 +1,5 @@ +import 'dart:ui'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; @@ -5,40 +7,246 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:taskwarrior/app/modules/reports/views/reports_view_replica.dart'; import 'package:taskwarrior/app/utils/app_settings/app_settings.dart'; import 'package:taskwarrior/app/modules/home/controllers/home_controller.dart'; -import 'package:taskwarrior/app/modules/home/views/home_page_nav_drawer_menu_item.dart'; import 'package:taskwarrior/app/modules/home/views/theme_clipper.dart'; import 'package:taskwarrior/app/modules/reports/views/reports_view_taskc.dart'; import 'package:taskwarrior/app/routes/app_pages.dart'; import 'package:taskwarrior/app/utils/constants/constants.dart'; import 'package:taskwarrior/app/utils/constants/taskwarrior_fonts.dart'; import 'package:taskwarrior/app/utils/constants/utilites.dart'; +import 'package:taskwarrior/app/utils/gen/assets.gen.dart'; import 'package:taskwarrior/app/utils/language/sentence_manager.dart'; import 'package:taskwarrior/app/utils/themes/theme_extension.dart'; import 'package:taskwarrior/app/utils/themes/dark_theme.dart'; import 'package:taskwarrior/app/utils/themes/light_theme.dart'; +/// A smooth animated moon/sun theme toggle slider. +class _ThemeToggleSlider extends StatelessWidget { + final bool isDarkMode; + final ValueChanged onChanged; + + const _ThemeToggleSlider({ + required this.isDarkMode, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + final tColors = Theme.of(context).extension()!; + + // Track background: dark = deep navy, light = pale amber + final trackColor = isDarkMode + ? const Color(0xFF1E2340) + : const Color(0xFFFFF3CC); + + // Thumb color: dark = soft indigo, light = warm amber + final thumbColor = isDarkMode + ? const Color(0xFF7C83FD) + : const Color(0xFFFFA726); + + const double trackW = 56; + const double trackH = 28; + const double thumbD = 22; + const double padding = 3; + + return GestureDetector( + onTap: () => onChanged(!isDarkMode), + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + width: trackW, + height: trackH, + decoration: BoxDecoration( + color: trackColor, + borderRadius: BorderRadius.circular(trackH / 2), + border: Border.all( + color: isDarkMode + ? const Color(0xFF3A3F6A) + : const Color(0xFFFFD54F), + width: 1, + ), + ), + child: Stack( + alignment: Alignment.centerLeft, + children: [ + // Sun icon (right side) + Positioned( + right: padding + 1, + child: AnimatedOpacity( + opacity: isDarkMode ? 0.0 : 0.0, + duration: const Duration(milliseconds: 200), + child: Icon( + Icons.wb_sunny_rounded, + size: 14, + color: const Color(0xFFFFA726), + ), + ), + ), + // Moon icon (left side) + Positioned( + left: padding + 1, + child: AnimatedOpacity( + opacity: isDarkMode ? 0.0 : 0.0, + duration: const Duration(milliseconds: 200), + child: Icon( + Icons.nightlight_round, + size: 14, + color: const Color(0xFF7C83FD), + ), + ), + ), + // Animated thumb with icon + AnimatedAlign( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + alignment: isDarkMode + ? Alignment.centerRight + : Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: padding), + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + width: thumbD, + height: thumbD, + decoration: BoxDecoration( + color: thumbColor, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: thumbColor.withOpacity(0.4), + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + child: Icon( + isDarkMode + ? Icons.nightlight_round + : Icons.wb_sunny_rounded, + key: ValueKey(isDarkMode), + size: 14, + color: Colors.white, + ), + ), + ), + ), + ), + ], + ), + ), + ); + } +} + +/// A single nav item — flat, no card shadow, just clean tap feedback. +class _NavItem extends StatelessWidget { + final IconData icon; + final String label; + final VoidCallback onTap; + final Color? iconColor; + final Color? textColor; + final bool isDestructive; + + const _NavItem({ + required this.icon, + required this.label, + required this.onTap, + this.iconColor, + this.textColor, + this.isDestructive = false, + }); + + @override + Widget build(BuildContext context) { + final tColors = Theme.of(context).extension()!; + final resolvedIconColor = iconColor ?? + (isDestructive ? TaskWarriorColors.red : tColors.primaryTextColor); + final resolvedTextColor = textColor ?? + (isDestructive ? TaskWarriorColors.red : tColors.primaryTextColor); + + return Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(10), + splashColor: resolvedIconColor!.withOpacity(0.08), + highlightColor: resolvedIconColor.withOpacity(0.05), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 13), + child: Row( + children: [ + Icon(icon, size: 20, color: resolvedIconColor), + const SizedBox(width: 14), + Expanded( + child: Text( + label, + style: TextStyle( + fontSize: 14.5, + fontWeight: TaskWarriorFonts.medium, + color: resolvedTextColor, + letterSpacing: 0.1, + ), + ), + ), + if (!isDestructive) + Icon( + Icons.chevron_right_rounded, + size: 18, + color: tColors.primaryDisabledTextColor?.withOpacity(0.4), + ), + ], + ), + ), + ), + ); + } +} + class NavDrawer extends StatelessWidget { final HomeController homeController; const NavDrawer({super.key, required this.homeController}); @override Widget build(BuildContext context) { - TaskwarriorColorTheme tColors = - Theme.of(context).extension()!; + final tColors = Theme.of(context).extension()!; + return Drawer( backgroundColor: tColors.dialogBackgroundColor, surfaceTintColor: tColors.dialogBackgroundColor, - child: Container( - color: tColors.dialogBackgroundColor, - child: ListView( - padding: EdgeInsets.zero, + elevation: 0, + child: SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - color: tColors.dialogBackgroundColor, - padding: const EdgeInsets.only(top: 50, left: 15, right: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + // ── Header ────────────────────────────────────────────────── + Padding( + padding: const EdgeInsets.fromLTRB(20, 24, 20, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Logo row + theme toggle + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Assets.svg.logo.svg(height: 40, width: 40), + const Spacer(), + // Moon/Sun slider toggle + _ThemeToggleSlider( + isDarkMode: AppSettings.isDarkMode, + onChanged: (bool newMode) async { + Get.changeThemeMode( + newMode ? ThemeMode.dark : ThemeMode.light); + AppSettings.isDarkMode = newMode; + await SelectedTheme.saveMode(AppSettings.isDarkMode); + homeController.initLanguageAndDarkMode(); + Get.changeTheme( + AppSettings.isDarkMode ? darkTheme : lightTheme); + }, + ), + ], + ), + const SizedBox(height: 22), Obx(() => Text( SentenceManager( currentLanguage: @@ -46,135 +254,160 @@ class NavDrawer extends StatelessWidget { .sentences .homePageMenu, style: TextStyle( - fontSize: TaskWarriorFonts.fontSizeExtraLarge, + fontSize: 22, fontWeight: TaskWarriorFonts.bold, color: tColors.primaryTextColor, + letterSpacing: -0.3, + height: 1.1, ), )), - Padding( - padding: const EdgeInsets.only(right: 10), - child: ThemeSwitcherClipper( - isDarkMode: AppSettings.isDarkMode, - onTap: (bool newMode) async { - Get.changeThemeMode( - newMode ? ThemeMode.dark : ThemeMode.light); - AppSettings.isDarkMode = newMode; - await SelectedTheme.saveMode(AppSettings.isDarkMode); - // Get.back(); - homeController.initLanguageAndDarkMode(); - Get.changeTheme( - AppSettings.isDarkMode ? darkTheme : lightTheme); - }, - child: Icon( - tColors.icons, - color: tColors.primaryTextColor, - size: 15, - ), + const SizedBox(height: 4), + // Subtle tagline / app name + Text( + 'Taskwarrior', + style: TextStyle( + fontSize: 12, + fontWeight: TaskWarriorFonts.regular, + color: tColors.primaryDisabledTextColor?.withOpacity(0.5), + letterSpacing: 0.8, ), ), + const SizedBox(height: 20), ], ), ), - Container( - color: tColors.dialogBackgroundColor, - height: Get.height * 0.03, - ), - Obx( - () => NavDrawerMenuItem( - icon: Icons.person_rounded, - text: SentenceManager( - currentLanguage: homeController.selectedLanguage.value, - ).sentences.navDrawerProfile, - onTap: () { - Get.toNamed(Routes.PROFILE); - }, - ), - ), - Obx( - () => Visibility( - visible: !homeController.taskchampion.value && - !homeController.taskReplica.value, - child: NavDrawerMenuItem( - icon: Icons.summarize, - text: SentenceManager( - currentLanguage: homeController.selectedLanguage.value, - ).sentences.navDrawerReports, - onTap: () { - Get.toNamed(Routes.REPORTS); - }, - ), - ), - ), - Obx( - () => Visibility( - visible: homeController.taskchampion.value && - !homeController.taskReplica.value, - child: NavDrawerMenuItem( - icon: Icons.summarize, - text: SentenceManager( - currentLanguage: homeController.selectedLanguage.value, - ).sentences.navDrawerReports, - onTap: () { - Get.to(() => ReportsHomeTaskc()); - }, - ), - ), - ), - Obx( - () => Visibility( - visible: !homeController.taskchampion.value && - homeController.taskReplica.value, - child: NavDrawerMenuItem( - icon: Icons.summarize, - text: SentenceManager( - currentLanguage: homeController.selectedLanguage.value, - ).sentences.navDrawerReports, - onTap: () { - Get.to(() => ReportsHomeReplica()); - }, - ), - ), - ), - Obx( - () => NavDrawerMenuItem( - icon: Icons.info, - text: SentenceManager( - currentLanguage: homeController.selectedLanguage.value, - ).sentences.navDrawerAbout, - onTap: () { - Get.toNamed(Routes.ABOUT); - }, + + // Thin separator + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Divider( + color: tColors.primaryTextColor!.withOpacity(0.07), + height: 1, + thickness: 1, ), ), - Obx( - () => NavDrawerMenuItem( - icon: Icons.settings, - text: SentenceManager( - currentLanguage: homeController.selectedLanguage.value, - ).sentences.navDrawerSettings, - onTap: () async { - final SharedPreferences prefs = - await SharedPreferences.getInstance(); - homeController.syncOnStart.value = - prefs.getBool('sync-onStart') ?? false; - homeController.syncOnTaskCreate.value = - prefs.getBool('sync-OnTaskCreate') ?? false; - homeController.delaytask.value = - prefs.getBool('delaytask') ?? false; - - Get.toNamed(Routes.SETTINGS); - }, + + // ── Nav Items ──────────────────────────────────────────────── + Expanded( + child: ListView( + padding: const EdgeInsets.fromLTRB(8, 12, 8, 12), + children: [ + // Profile + Obx(() => _NavItem( + icon: Icons.person_outline_rounded, + label: SentenceManager( + currentLanguage: + homeController.selectedLanguage.value, + ).sentences.navDrawerProfile, + onTap: () => Get.toNamed(Routes.PROFILE), + )), + + // Reports — default (no sync backend) + Obx(() => Visibility( + visible: !homeController.taskchampion.value && + !homeController.taskReplica.value, + child: _NavItem( + icon: Icons.bar_chart_rounded, + label: SentenceManager( + currentLanguage: + homeController.selectedLanguage.value, + ).sentences.navDrawerReports, + onTap: () => Get.toNamed(Routes.REPORTS), + ), + )), + + // Reports — taskchampion + Obx(() => Visibility( + visible: homeController.taskchampion.value && + !homeController.taskReplica.value, + child: _NavItem( + icon: Icons.bar_chart_rounded, + label: SentenceManager( + currentLanguage: + homeController.selectedLanguage.value, + ).sentences.navDrawerReports, + onTap: () => Get.to(() => ReportsHomeTaskc()), + ), + )), + + // Reports — replica + Obx(() => Visibility( + visible: !homeController.taskchampion.value && + homeController.taskReplica.value, + child: _NavItem( + icon: Icons.bar_chart_rounded, + label: SentenceManager( + currentLanguage: + homeController.selectedLanguage.value, + ).sentences.navDrawerReports, + onTap: () => Get.to(() => ReportsHomeReplica()), + ), + )), + + // Section divider + Padding( + padding: + const EdgeInsets.symmetric(vertical: 8, horizontal: 4), + child: Divider( + color: tColors.primaryTextColor!.withOpacity(0.07), + height: 1, + thickness: 1, + ), + ), + + // About + Obx(() => _NavItem( + icon: Icons.info_outline_rounded, + label: SentenceManager( + currentLanguage: + homeController.selectedLanguage.value, + ).sentences.navDrawerAbout, + onTap: () => Get.toNamed(Routes.ABOUT), + )), + + // Settings + Obx(() => _NavItem( + icon: Icons.tune_rounded, + label: SentenceManager( + currentLanguage: + homeController.selectedLanguage.value, + ).sentences.navDrawerSettings, + onTap: () async { + final prefs = await SharedPreferences.getInstance(); + homeController.syncOnStart.value = + prefs.getBool('sync-onStart') ?? false; + homeController.syncOnTaskCreate.value = + prefs.getBool('sync-OnTaskCreate') ?? false; + homeController.delaytask.value = + prefs.getBool('delaytask') ?? false; + Get.toNamed(Routes.SETTINGS); + }, + )), + ], ), ), - Obx( - () => NavDrawerMenuItem( - icon: Icons.exit_to_app, - text: SentenceManager( - currentLanguage: homeController.selectedLanguage.value, - ).sentences.navDrawerExit, - onTap: () { - _showExitConfirmationDialog(context); - }, + + // ── Footer / Exit ───────────────────────────────────────────── + Padding( + padding: const EdgeInsets.fromLTRB(8, 0, 8, 16), + child: Column( + children: [ + Divider( + color: tColors.primaryTextColor!.withOpacity(0.07), + height: 1, + thickness: 1, + ), + const SizedBox(height: 8), + Obx(() => _NavItem( + icon: Icons.logout_rounded, + label: SentenceManager( + currentLanguage: + homeController.selectedLanguage.value, + ).sentences.navDrawerExit, + onTap: () => _showExitConfirmationDialog(context), + isDestructive: true, + )), + ], ), ), ], @@ -184,12 +417,10 @@ class NavDrawer extends StatelessWidget { } Future _showExitConfirmationDialog(BuildContext context) async { - TaskwarriorColorTheme tColors = - Theme.of(context).extension()!; + final tColors = Theme.of(context).extension()!; return showDialog( context: context, - barrierDismissible: - false, // Prevents closing the dialog by tapping outside + barrierDismissible: false, builder: (BuildContext context) { return Utils.showAlertDialog( title: Text( @@ -197,18 +428,14 @@ class NavDrawer extends StatelessWidget { currentLanguage: homeController.selectedLanguage.value) .sentences .homePageExitApp, - style: TextStyle( - color: tColors.primaryTextColor, - ), + style: TextStyle(color: tColors.primaryTextColor), ), content: Text( SentenceManager( currentLanguage: homeController.selectedLanguage.value) .sentences .homePageAreYouSureYouWantToExit, - style: TextStyle( - color: tColors.primaryTextColor, - ), + style: TextStyle(color: tColors.primaryTextColor), ), actions: [ TextButton( @@ -217,13 +444,9 @@ class NavDrawer extends StatelessWidget { currentLanguage: homeController.selectedLanguage.value) .sentences .homePageCancel, - style: TextStyle( - color: tColors.primaryTextColor, - ), + style: TextStyle(color: tColors.primaryTextColor), ), - onPressed: () { - Navigator.of(context).pop(); // Close the dialog - }, + onPressed: () => Navigator.of(context).pop(), ), TextButton( child: Text( @@ -231,13 +454,11 @@ class NavDrawer extends StatelessWidget { currentLanguage: homeController.selectedLanguage.value) .sentences .homePageExit, - style: TextStyle( - color: tColors.primaryTextColor, - ), + style: TextStyle(color: TaskWarriorColors.red), ), onPressed: () { - Navigator.of(context).pop(); // Close the dialog - SystemNavigator.pop(); // Exit the app + Navigator.of(context).pop(); + SystemNavigator.pop(); }, ), ], @@ -245,4 +466,4 @@ class NavDrawer extends StatelessWidget { }, ); } -} +} \ No newline at end of file diff --git a/lib/app/modules/home/views/tas_list_item.dart b/lib/app/modules/home/views/tas_list_item.dart index 6c6041bc..3f2fa41d 100644 --- a/lib/app/modules/home/views/tas_list_item.dart +++ b/lib/app/modules/home/views/tas_list_item.dart @@ -8,6 +8,8 @@ import 'package:taskwarrior/app/utils/taskfunctions/datetime_differences.dart'; import 'package:taskwarrior/app/utils/taskfunctions/modify.dart'; import 'package:taskwarrior/app/utils/taskfunctions/urgency.dart'; import 'package:taskwarrior/app/utils/themes/theme_extension.dart'; +import 'package:taskwarrior/app/utils/priority/priority.dart'; +import 'package:taskwarrior/app/widgets/pill.dart'; class TaskListItem extends StatelessWidget { const TaskListItem( @@ -27,199 +29,307 @@ class TaskListItem extends StatelessWidget { final bool useDelayTask; final SupportedLanguage selectedLanguage; + // ── Due state helpers ────────────────────────────────────────────────────── + + bool _isDueWithinOneDay(DateTime due) { + final diff = due.difference(DateTime.now()); + return diff.inDays < 1 && diff.inMicroseconds > 0; + } + + bool _isOverdue(DateTime due) => + due.difference(DateTime.now()).inMicroseconds < 0; + + // ── Build ────────────────────────────────────────────────────────────────── + @override Widget build(BuildContext context) { - TaskwarriorColorTheme tColors = Theme.of(context).extension()!; - // ignore: unused_element - void saveChanges() async { - var now = DateTime.now().toUtc(); - modify.save( - modified: () => now, - ); - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text( - SentenceManager(currentLanguage: selectedLanguage) - .sentences - .taskUpdated, - style: TextStyle( - color: tColors.primaryTextColor, - ), - ), - backgroundColor: tColors.secondaryBackgroundColor, - duration: const Duration(seconds: 2))); - } + final tColors = Theme.of(context).extension()!; - bool isDueWithinOneDay(DateTime dueDate) { - DateTime now = DateTime.now(); - Duration difference = dueDate.difference(now); - return difference.inDays < 1 && difference.inMicroseconds > 0; - } + final bool isPending = task.status[0].toUpperCase() == 'P'; + final p = getPriorityStyle(task.priority); + final sentences = + SentenceManager(currentLanguage: selectedLanguage).sentences; - bool isOverDue(DateTime dueDate) { - DateTime now = DateTime.now(); - Duration difference = dueDate.difference(now); - return difference.inMicroseconds < 0; - } + final Color textColor = tColors.primaryTextColor!; + final Color subColor = tColors.dimCol!; + + // Due urgency states + final bool overdue = + task.due != null && _isOverdue(task.due!) && useDelayTask; + final bool dueSoon = + task.due != null && _isDueWithinOneDay(task.due!) && useDelayTask; + + // Left accent bar: priority color, overridden by due urgency + Color accentColor = p.accent; + if (dueSoon && !overdue) accentColor = const Color(0xFFFFA726); + if (overdue) accentColor = const Color(0xFFEF5350); - MaterialColor colours = Colors.grey; - Color colour =tColors.primaryTextColor!; - Color dimColor = tColors.dimCol!; - if (task.priority == 'H') { - colours = Colors.red; - } else if (task.priority == 'M') { - colours = Colors.yellow; - } else if (task.priority == 'L') { - colours = Colors.green; + // Subtle card tint when overdue + final Color cardBg = overdue + ? const Color(0xFFEF5350).withOpacity(0.05) + : tColors.primaryBackgroundColor!; + + // Urgency score → color ramp + final double urgencyVal = urgency(task); + Color urgencyColor; + if (urgencyVal >= 10) { + urgencyColor = const Color(0xFFEF5350); + } else if (urgencyVal >= 5) { + urgencyColor = const Color(0xFFFFA726); + } else { + urgencyColor = subColor.withOpacity(0.7); } - if ((task.status[0].toUpperCase()) == 'P') { - // Pending tasks - return Container( - decoration: BoxDecoration( - border: Border.all( - color: (task.due != null && - isDueWithinOneDay(task.due!) && - useDelayTask) - ? Colors - .red // Set border color to red if due within 1 day and useDelayTask is true - : dimColor, // Set default border color - ), - borderRadius: BorderRadius.circular(8.0), - color: (task.due != null && isOverDue(task.due!) && useDelayTask) - ? Colors.red.withAlpha(50) - : tColors.primaryBackgroundColor, + // Metadata strings + final String modifiedStr = task.modified != null + ? age(task.modified!) + : (task.start != null ? age(task.start!) : '-'); + final String dueStr = task.due != null ? when(task.due!) : '-'; + + return Container( + margin: const EdgeInsets.symmetric(vertical: 4), + decoration: BoxDecoration( + color: cardBg, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: overdue + ? const Color(0xFFEF5350).withOpacity(0.3) + : dueSoon + ? const Color(0xFFFFA726).withOpacity(0.3) + : subColor.withOpacity(0.12), + width: 1, ), - child: ListTile( - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - CircleAvatar( - backgroundColor: colours, - radius: 8, - ), - const SizedBox(width: 8), - SizedBox( - width: MediaQuery.of(context).size.width * 0.70, - child: Text( - '${(task.id == 0) ? '#' : task.id}. ${task.description}', - maxLines: 1, - overflow: TextOverflow.ellipsis, - // style: GoogleFonts.poppins( - // color: colour, - // ), - style: TextStyle( - fontFamily: FontFamily.poppins, color: colour), + ), + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(12), + child: InkWell( + borderRadius: BorderRadius.circular(12), + splashColor: accentColor.withOpacity(0.06), + highlightColor: accentColor.withOpacity(0.04), + child: IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // ── Left accent bar ────────────────────────────────────── + Container( + width: 4, + decoration: BoxDecoration( + color: accentColor, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + bottomLeft: Radius.circular(12), ), ), - ], - ), - Text( - (task.annotations != null) - ? ' [${task.annotations!.length}]' - : '', - // style: GoogleFonts.poppins( - // color: colour, - // ), - style: TextStyle(fontFamily: FontFamily.poppins, color: colour), - ), - ], - ), - subtitle: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Text( - '${pendingFilter ? '' : '${task.status[0].toUpperCase()}\n'}' - '${SentenceManager(currentLanguage: selectedLanguage).sentences.homePageLastModified} : ${(task.modified != null) ? age(task.modified!) : ((task.start != null) ? age(task.start!) : '-')} | ' - '${SentenceManager(currentLanguage: selectedLanguage).sentences.homePageDue} : ${(task.due != null) ? when(task.due!) : '-'}' - .replaceFirst(RegExp(r' \[\]$'), '') - .replaceAll(RegExp(r' +'), ' '), - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontFamily: FontFamily.poppins, - color: dimColor, - fontSize: TaskWarriorFonts.fontSizeSmall), - ), ), - ), - Text( - formatUrgency(urgency(task)), - // style: GoogleFonts.poppins( - // color: colour, - // ), - style: TextStyle(fontFamily: FontFamily.poppins, color: colour), - ), - ], - ), - ), - ); - } else { - // Completed tasks - return ListTile( - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - CircleAvatar( - backgroundColor: colours, - radius: 8, - ), - const SizedBox(width: 8), - SizedBox( - width: MediaQuery.of(context).size.width * 0.65, - child: Text( - '${(task.id == 0) ? '#' : task.id}. ${task.description}', - maxLines: 1, - overflow: TextOverflow.ellipsis, - // style: GoogleFonts.poppins( - // color: colour, - // ), - style: TextStyle( - fontFamily: FontFamily.poppins, color: colour), + + // ── Card content ───────────────────────────────────────── + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(12, 11, 12, 11), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // ── Row 1: Task ID + description + annotation count + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // ID badge + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: accentColor.withOpacity(0.12), + borderRadius: BorderRadius.circular(5), + ), + child: Text( + '#${task.id == 0 ? '-' : task.id}', + style: TextStyle( + fontFamily: FontFamily.poppins, + fontSize: 10, + fontWeight: TaskWarriorFonts.semiBold, + color: accentColor, + letterSpacing: 0.3, + ), + ), + ), + const SizedBox(width: 8), + + // Description + Expanded( + child: Text( + task.description, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontFamily: FontFamily.poppins, + fontSize: 13.5, + fontWeight: isPending + ? TaskWarriorFonts.medium + : TaskWarriorFonts.regular, + color: isPending + ? textColor + : textColor.withOpacity(0.45), + decoration: isPending + ? TextDecoration.none + : TextDecoration.lineThrough, + decorationColor: subColor.withOpacity(0.4), + height: 1.35, + ), + ), + ), + + // Annotation count + if (task.annotations != null && + task.annotations!.isNotEmpty) ...[ + const SizedBox(width: 6), + Pill( + label: '${task.annotations!.length}', + bg: subColor.withOpacity(0.1), + fg: subColor, + icon: Icons.comment_outlined, + ), + ], + ], + ), + + const SizedBox(height: 8), + + // ── Row 2: Priority pill + due badge + urgency score + Row( + children: [ + Pill( + label: p.label, + bg: p.chipBg, + fg: p.chipFg, + ), + const SizedBox(width: 6), + + if (overdue) + Pill( + label: 'Overdue', + bg: const Color(0x15EF5350), + fg: const Color(0xFFEF5350), + icon: Icons.warning_amber_rounded, + ) + else if (dueSoon) + Pill( + label: 'Due soon', + bg: const Color(0x15FFA726), + fg: const Color(0xFFFFA726), + icon: Icons.schedule_rounded, + ), + + const Spacer(), + + // Urgency value + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.bolt_rounded, + size: 12, + color: Colors.white, + ), + const SizedBox(width: 2), + Text( + formatUrgency(urgencyVal), + style: TextStyle( + fontFamily: FontFamily.poppins, + fontSize: 11, + fontWeight: TaskWarriorFonts.semiBold, + color: Colors.white, + letterSpacing: 0.1, + ), + ), + ], + ), + ], + ), + + const SizedBox(height: 7), + + // ── Row 3: Modified + Due metadata ──────────────── + Row( + children: [ + // Modified + Icon( + Icons.edit_outlined, + size: 11, + color: subColor.withOpacity(0.5), + ), + const SizedBox(width: 3), + Flexible( + child: Text( + modifiedStr, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontFamily: FontFamily.poppins, + fontSize: TaskWarriorFonts.fontSizeSmall, + color: subColor.withOpacity(0.6), + ), + ), + ), + + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 7), + child: Text( + '·', + style: TextStyle( + fontFamily: FontFamily.poppins, + color: subColor.withOpacity(0.25), + fontSize: 13, + ), + ), + ), + + // Due + Icon( + Icons.calendar_today_outlined, + size: 11, + color: overdue + ? const Color(0xFFEF5350).withOpacity(0.6) + : subColor.withOpacity(0.5), + ), + const SizedBox(width: 3), + Flexible( + child: Text( + dueStr, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontFamily: FontFamily.poppins, + fontSize: TaskWarriorFonts.fontSizeSmall, + color: overdue + ? const Color(0xFFEF5350) + .withOpacity(0.75) + : subColor.withOpacity(0.6), + ), + ), + ), + + // Status badge — shown for completed/waiting tasks + if (!pendingFilter && !isPending) ...[ + const Spacer(), + Pill( + label: task.status[0].toUpperCase() + + task.status.substring(1).toLowerCase(), + bg: subColor.withOpacity(0.08), + fg: subColor.withOpacity(0.55), + ), + ], + ], + ), + ], + ), ), ), ], ), - ], - ), - subtitle: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Text( - '${SentenceManager(currentLanguage: selectedLanguage).sentences.homePageLastModified} :${(task.modified != null) ? age(task.modified!) : ((task.start != null) ? age(task.start!) : '-')} | ' - '${SentenceManager(currentLanguage: selectedLanguage).sentences.homePageDue} : ${(task.due != null) ? when(task.due!) : '-'}' - .replaceFirst(RegExp(r' \[\]$'), '') - .replaceAll(RegExp(r' +'), ' '), - overflow: TextOverflow.ellipsis, - // style: GoogleFonts.poppins( - // color: dimColor, - // fontSize: TaskWarriorFonts.fontSizeSmall, - // ), - style: TextStyle( - fontFamily: FontFamily.poppins, - color: dimColor, - fontSize: TaskWarriorFonts.fontSizeSmall), - ), - ), - ), - Text( - formatUrgency(urgency(task)), - // style: GoogleFonts.poppins( - // color: colour, - // ), - style: TextStyle(fontFamily: FontFamily.poppins, color: colour), - ), - ], + ), ), - ); - } + ), + ); } } diff --git a/lib/app/utils/priority/priority.dart b/lib/app/utils/priority/priority.dart new file mode 100644 index 00000000..9f3f6009 --- /dev/null +++ b/lib/app/utils/priority/priority.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; + +class PriorityStyle { + final Color accent; + final Color chipBg; + final Color chipFg; + final String label; + + const PriorityStyle({ + required this.accent, + required this.chipBg, + required this.chipFg, + required this.label, + }); +} + +PriorityStyle getPriorityStyle(String? priority) { + switch (priority) { + case 'H': + return const PriorityStyle( + accent: Color(0xFFEF5350), + chipBg: Color(0x15EF5350), + chipFg: Color(0xFFEF5350), + label: 'High', + ); + case 'M': + return const PriorityStyle( + accent: Color(0xFFFFA726), + chipBg: Color(0x15FFA726), + chipFg: Color(0xFFFFA726), + label: 'Med', + ); + case 'L': + return const PriorityStyle( + accent: Color(0xFF66BB6A), + chipBg: Color(0x1566BB6A), + chipFg: Color(0xFF66BB6A), + label: 'Low', + ); + default: + return const PriorityStyle( + accent: Color(0xFF78909C), + chipBg: Color(0x1278909C), + chipFg: Color(0xFF78909C), + label: 'None', + ); + } +} diff --git a/lib/app/widgets/pill.dart b/lib/app/widgets/pill.dart new file mode 100644 index 00000000..3bf1c128 --- /dev/null +++ b/lib/app/widgets/pill.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:taskwarrior/app/utils/constants/taskwarrior_fonts.dart'; +import 'package:taskwarrior/app/utils/gen/fonts.gen.dart'; + +class Pill extends StatelessWidget { + final String label; + final Color bg; + final Color fg; + final IconData? icon; + + const Pill({ + super.key, + required this.label, + required this.bg, + required this.fg, + this.icon, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3), + decoration: BoxDecoration( + color: bg, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (icon != null) ...[ + Icon(icon, size: 10, color: fg), + const SizedBox(width: 3), + ], + Text( + label, + style: TextStyle( + fontFamily: FontFamily.poppins, + fontSize: 10, + fontWeight: TaskWarriorFonts.semiBold, + color: fg, + letterSpacing: 0.2, + ), + ), + ], + ), + ); + } +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 13807bb2..e20e7221 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include void fl_register_plugins(FlPluginRegistry* registry) { @@ -17,6 +18,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) flutter_timezone_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterTimezonePlugin"); flutter_timezone_plugin_register_with_registrar(flutter_timezone_registrar); + g_autoptr(FlPluginRegistrar) gtk_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); + gtk_plugin_register_with_registrar(gtk_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index b01d1fd9..fe92fa4d 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_linux flutter_timezone + gtk url_launcher_linux ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index cb98b370..91b0fc33 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,7 @@ import FlutterMacOS import Foundation +import app_links import connectivity_plus import file_picker import file_picker_writable @@ -18,6 +19,7 @@ import sqflite_darwin import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FilePickerWritablePlugin.register(with: registry.registrar(forPlugin: "FilePickerWritablePlugin")) diff --git a/pubspec.lock b/pubspec.lock index c7182030..e58074e4 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: @@ -1141,10 +1197,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/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 9d16245a..7bbcffce 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,7 @@ #include "generated_plugin_registrant.h" +#include #include #include #include @@ -13,6 +14,8 @@ #include void RegisterPlugins(flutter::PluginRegistry* registry) { + AppLinksPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AppLinksPluginCApi")); ConnectivityPlusWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); FileSelectorWindowsRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 50ed42d3..79ba045e 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + app_links connectivity_plus file_selector_windows flutter_timezone