From b88f484bc3fce49bd53b88afd85360d9d680b2e7 Mon Sep 17 00:00:00 2001 From: Amoghhosamane Date: Thu, 5 Mar 2026 22:49:56 +0530 Subject: [PATCH 1/2] feat: add delete all button to history #65 --- analyze_out_new.txt | Bin 0 -> 200 bytes lib/constants/colours.dart | 3 + lib/widgets/history.dart | 81 +++++++++++++++++- .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 5 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 analyze_out_new.txt diff --git a/analyze_out_new.txt b/analyze_out_new.txt new file mode 100644 index 0000000000000000000000000000000000000000..41c1fd340609197ef70a021bd573a0e17b11fe14 GIT binary patch literal 200 zcmezW&ygXIA(0`6p^~ABArr_>XHWpsc?_uxMGOTD`3xxxdSIvkl%YQ4W#D4)1KI+z vwHOFXf%X;yd1*kgQlNWM7!-j#4WL^Rfjo%Y6c`K{%z { ElevatedButton( onPressed: () => Navigator.pop(context, true), style: ElevatedButton.styleFrom( - backgroundColor: Colors.redAccent.withValues(alpha: 0.1), - foregroundColor: Colors.redAccent, + backgroundColor: colours.error.withValues(alpha: 0.1), + foregroundColor: colours.error, elevation: 0, ), child: const Text('Delete'), @@ -182,6 +183,70 @@ class _HistoryState extends State { } } + Future _deleteAllSessions() async { + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Delete All Sessions'), + content: const Text( + 'Are you sure you want to delete ALL sessions? This action cannot be undone.', + style: TextStyle(fontSize: 16), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: () => Navigator.pop(context, true), + style: ElevatedButton.styleFrom( + backgroundColor: colours.error.withValues(alpha: 0.1), + foregroundColor: colours.error, + elevation: 0, + ), + child: const Text('Delete All'), + ), + ], + ), + ); + + if (confirmed == true) { + await _performDeleteAll(); + } + } + + Future _performDeleteAll() async { + setState(() => _isLoading = true); + try { + final newContent = serializeSessions([]); + await writePod( + 'sessions.ttl', + newContent, + overwrite: true, + ); + await _loadSessions(); + } on SecurityKeyNotAvailableException { + debugPrint( + 'Security key missing - cannot write sessions.ttl for bulk deletion', + ); + if (mounted) { + await getKeyFromUserIfRequired(context, widget); + if (mounted) { + await _performDeleteAll(); + } + } + } catch (e) { + debugPrint('Error deleting all sessions: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to delete all sessions: $e')), + ); + } + } finally { + if (mounted) setState(() => _isLoading = false); + } + } + Future _editSession(Map session) async { final titleController = TextEditingController(text: session['title']); final descriptionController = @@ -280,8 +345,18 @@ class _HistoryState extends State { title: const Text('Session History'), automaticallyImplyLeading: false, // Don't show back button actions: [ + if (_sessions.isNotEmpty) + IconButton( + icon: const Icon( + Icons.delete_sweep_outlined, + color: colours.error, + ), + tooltip: 'Delete all sessions', + onPressed: _deleteAllSessions, + ), IconButton( icon: const Icon(Icons.refresh), + tooltip: 'Refresh', onPressed: _loadSessions, ), const SizedBox(width: 8), @@ -411,7 +486,7 @@ class _HistoryState extends State { icon: const Icon( Icons.delete_outline, size: 20, - color: Colors.redAccent, + color: colours.error, ), onPressed: () => _deleteSession(session['rawStart']!), diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index e8e5160..041bd40 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -9,6 +9,7 @@ #include #include #include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { @@ -18,6 +19,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("FastRsaPlugin")); FlutterSecureStorageWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); + PrintingPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PrintingPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index edc357f..ff1f580 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST audioplayers_windows fast_rsa flutter_secure_storage_windows + printing url_launcher_windows ) From 75f660ef826729dffc233e16cec9f033ecf1a4ef Mon Sep 17 00:00:00 2001 From: Amoghhosamane Date: Fri, 6 Mar 2026 23:02:14 +0530 Subject: [PATCH 2/2] feat: Support Light/Dark mode #66 This feature adds light and dark mode support with a toggle button as per Issue #66. Details: - Integrated theme toggle in AppBar. - Updated circular timer with theme-aware colors. - Improved AppButton colors for visibility in Dark mode. - Fixed linting issues and strict TODO check for Makefile prep. --- lib/constants/colours.dart | 28 ++- lib/home.dart | 24 ++- lib/main.dart | 167 ++++++++++++------ lib/widgets/app_button.dart | 28 ++- lib/widgets/app_circular_countdown_timer.dart | 11 +- lib/widgets/history.dart | 14 +- lib/widgets/timer.dart | 37 +++- 7 files changed, 233 insertions(+), 76 deletions(-) diff --git a/lib/constants/colours.dart b/lib/constants/colours.dart index b8eda99..ef40ebf 100644 --- a/lib/constants/colours.dart +++ b/lib/constants/colours.dart @@ -33,10 +33,36 @@ import 'package:flutter/material.dart'; // const background = Color(0xFFF5E0C8); const background = Color(0xFFF0D1AD); +const backgroundDark = Color(0xFF1A1A1A); /// A lighter colour for the top and bottom (navbar) elements of the app. - const border = Color(0xFFF5E0C8); +const borderDark = Color(0xFF2C2C2C); /// A color for error messages or destructive actions. const error = Colors.redAccent; + +/// Theme management. +class ThemeNotifier extends ChangeNotifier { + static final ThemeNotifier _instance = ThemeNotifier._internal(); + factory ThemeNotifier() => _instance; + ThemeNotifier._internal(); + + ThemeMode _themeMode = ThemeMode.light; + ThemeMode get themeMode => _themeMode; + + bool get isDarkMode => _themeMode == ThemeMode.dark; + + void toggleTheme() { + _themeMode = + _themeMode == ThemeMode.light ? ThemeMode.dark : ThemeMode.light; + notifyListeners(); + } + + set themeMode(ThemeMode mode) { + _themeMode = mode; + notifyListeners(); + } +} + +final themeNotifier = ThemeNotifier(); diff --git a/lib/home.dart b/lib/home.dart index 543e4d1..1a06f59 100644 --- a/lib/home.dart +++ b/lib/home.dart @@ -130,6 +130,12 @@ class HomeState extends State { } } + void _toggleTheme() async { + themeNotifier.toggleTheme(); + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool('isDark', themeNotifier.isDarkMode); + } + final List _pages = [ const Timer(key: PageStorageKey('timer_page')), const Instructions(key: PageStorageKey('text_page')), @@ -160,7 +166,7 @@ class HomeState extends State { ), ], ), - backgroundColor: border, + backgroundColor: Theme.of(context).colorScheme.surface, actions: [ Center( child: GestureDetector( @@ -191,6 +197,22 @@ class HomeState extends State { ), ), const SizedBox(width: 8), + ListenableBuilder( + listenable: themeNotifier, + builder: (context, child) { + return IconButton( + icon: Icon( + themeNotifier.isDarkMode + ? Icons.light_mode_outlined + : Icons.dark_mode_outlined, + size: 24, + ), + onPressed: _toggleTheme, + tooltip: 'Toggle light/dark mode', + ); + }, + ), + const SizedBox(width: 8), IconButton( icon: const Icon(Icons.info_outline, size: 24), onPressed: () => showAppAboutDialog(context), diff --git a/lib/main.dart b/lib/main.dart index 93598a1..a42670f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -28,62 +28,129 @@ library; import 'package:flutter/material.dart'; import 'package:innerpod/home.dart'; +import 'package:innerpod/constants/colours.dart'; +import 'package:shared_preferences/shared_preferences.dart'; //import 'package:innerpod/timer.dart'; -void main() { +void main() async { WidgetsFlutterBinding.ensureInitialized(); - // Call MaterialApp() here rather than within InnerPod so that - // MaterialLocalizations is found when firing off the showAboutDialog. + final prefs = await SharedPreferences.getInstance(); + final isDark = prefs.getBool('isDark') ?? false; + themeNotifier.themeMode = isDark ? ThemeMode.dark : ThemeMode.light; - runApp( - MaterialApp( - title: 'Inner Pod', - theme: ThemeData( - useMaterial3: true, - colorScheme: ColorScheme.fromSeed( - seedColor: const Color(0xFFF0D1AD), - surface: const Color(0xFFFDF7F0), // Lighter cream surface - primary: const Color(0xFF8B5E3C), // Earthy brown/gold primary - onPrimary: Colors.white, - secondary: const Color(0xFFE6B276), - surfaceContainerHighest: const Color(0xFFF5E0C8), - ), - appBarTheme: const AppBarTheme( - centerTitle: true, - backgroundColor: Colors.transparent, - elevation: 0, - scrolledUnderElevation: 0, - titleTextStyle: TextStyle( - color: Color(0xFF5D4037), - fontSize: 22, - fontWeight: FontWeight.bold, - ), - ), - navigationBarTheme: NavigationBarThemeData( - backgroundColor: const Color(0xFFFDF7F0), - indicatorColor: const Color(0xFFE6B276).withValues(alpha: 0.5), - labelTextStyle: WidgetStateProperty.all( - const TextStyle(fontSize: 12, fontWeight: FontWeight.w500), + runApp(const InnerPodApp()); +} + +class InnerPodApp extends StatelessWidget { + const InnerPodApp({super.key}); + + @override + Widget build(BuildContext context) { + return ListenableBuilder( + listenable: themeNotifier, + builder: (context, child) { + return MaterialApp( + title: 'Inner Pod', + themeMode: themeNotifier.themeMode, + theme: ThemeData( + useMaterial3: true, + colorScheme: ColorScheme.fromSeed( + seedColor: const Color(0xFFF0D1AD), + surface: const Color(0xFFFDF7F0), + primary: const Color(0xFF8B5E3C), + onPrimary: Colors.white, + secondary: const Color(0xFFE6B276), + surfaceContainerHighest: const Color(0xFFF5E0C8), + ), + appBarTheme: const AppBarTheme( + centerTitle: true, + backgroundColor: Colors.transparent, + elevation: 0, + scrolledUnderElevation: 0, + titleTextStyle: TextStyle( + color: Color(0xFF5D4037), + fontSize: 22, + fontWeight: FontWeight.bold, + ), + ), + navigationBarTheme: NavigationBarThemeData( + backgroundColor: const Color(0xFFFDF7F0), + indicatorColor: const Color(0xFFE6B276).withValues(alpha: 0.5), + labelTextStyle: WidgetStateProperty.all( + const TextStyle(fontSize: 12, fontWeight: FontWeight.w500), + ), + ), + cardTheme: CardThemeData( + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + color: Colors.white.withValues(alpha: 0.9), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + padding: + const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + ), + ), ), - ), - cardTheme: CardThemeData( - elevation: 2, - shape: - RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), - color: Colors.white.withValues(alpha: 0.9), - ), - elevatedButtonTheme: ElevatedButtonThemeData( - style: ElevatedButton.styleFrom( - elevation: 0, - shape: - RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + darkTheme: ThemeData( + useMaterial3: true, + colorScheme: ColorScheme.fromSeed( + seedColor: const Color(0xFF8B5E3C), + brightness: Brightness.dark, + surface: const Color(0xFF1A1A1A), + primary: const Color(0xFFE6B276), + onPrimary: Colors.black, + secondary: const Color(0xFF8B5E3C), + surfaceContainerHighest: const Color(0xFF2C2C2C), + ), + appBarTheme: const AppBarTheme( + centerTitle: true, + backgroundColor: Colors.transparent, + elevation: 0, + scrolledUnderElevation: 0, + titleTextStyle: TextStyle( + color: Color(0xFFE6B276), + fontSize: 22, + fontWeight: FontWeight.bold, + ), + iconTheme: IconThemeData(color: Color(0xFFE6B276)), + ), + navigationBarTheme: NavigationBarThemeData( + backgroundColor: const Color(0xFF1A1A1A), + indicatorColor: const Color(0xFF8B5E3C).withValues(alpha: 0.5), + labelTextStyle: WidgetStateProperty.all( + const TextStyle(fontSize: 12, fontWeight: FontWeight.w500), + ), + ), + cardTheme: CardThemeData( + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + color: Colors.grey[900]?.withValues(alpha: 0.9), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + padding: + const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + ), + ), ), - ), - ), - home: const InnerPod(), - ), - ); + home: const InnerPod(), + ); + }, + ); + } } diff --git a/lib/widgets/app_button.dart b/lib/widgets/app_button.dart index 70942af..ddb1fb3 100644 --- a/lib/widgets/app_button.dart +++ b/lib/widgets/app_button.dart @@ -69,7 +69,11 @@ class AppButton extends StatelessWidget { @override Widget build(BuildContext context) { - bool isPrimary = backgroundColor != Colors.white; + bool isPrimary = backgroundColor != Colors.white && + backgroundColor != Colors.black && + backgroundColor != Colors.transparent; + + final isDark = Theme.of(context).brightness == Brightness.dark; return SizedBox( height: 52, @@ -83,18 +87,24 @@ class AppButton extends StatelessWidget { colors: isPrimary ? [ backgroundColor, - Color.lerp(backgroundColor, Colors.black, 0.05)!, + Color.lerp( + backgroundColor, + isDark ? Colors.white : Colors.black, + 0.15, + )!, ] : [ - Colors.white, - const Color(0xFFFDF7F0), + isDark ? const Color(0xFF2C2C2C) : Colors.white, + isDark ? const Color(0xFF1A1A1A) : const Color(0xFFFDF7F0), ], ), boxShadow: [ BoxShadow( color: isPrimary ? backgroundColor.withValues(alpha: 0.3) - : Colors.grey.withValues(alpha: 0.1), + : (isDark + ? Colors.black26 + : Colors.grey.withValues(alpha: 0.1)), blurRadius: 12, offset: const Offset(0, 4), ), @@ -114,10 +124,12 @@ class AppButton extends StatelessWidget { fontSize: fontSize, fontWeight: isPrimary ? FontWeight.bold : fontWeight, color: isPrimary - ? (Theme.of(context).brightness == Brightness.dark - ? Colors.white + ? (isDark + ? Colors.white.withValues(alpha: 0.9) : Colors.black87) - : const Color(0xFF5D4037), + : (isDark + ? const Color(0xFFF5E0C8) + : const Color(0xFF5D4037)), letterSpacing: 0.5, ), ), diff --git a/lib/widgets/app_circular_countdown_timer.dart b/lib/widgets/app_circular_countdown_timer.dart index a84d8fc..3e3bdeb 100644 --- a/lib/widgets/app_circular_countdown_timer.dart +++ b/lib/widgets/app_circular_countdown_timer.dart @@ -68,17 +68,22 @@ class AppCircularCountDownTimer extends StatelessWidget { @override Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + return CircularCountDownTimer( width: 250, height: 250, duration: duration, controller: controller, autoStart: false, - backgroundColor: background, - ringColor: _spin1, + backgroundColor: isDark ? backgroundDark : background, + ringColor: isDark ? const Color(0xFF2C2C2C) : _spin1, fillColor: _spin2, strokeWidth: 20.0, - textStyle: const TextStyle(color: _text, fontSize: 55), + textStyle: TextStyle( + color: isDark ? const Color(0xFFF5E0C8) : _text, + fontSize: 55, + ), onComplete: onComplete, isReverse: true, isReverseAnimation: true, diff --git a/lib/widgets/history.dart b/lib/widgets/history.dart index 03f2021..6c6fb83 100644 --- a/lib/widgets/history.dart +++ b/lib/widgets/history.dart @@ -404,7 +404,7 @@ class _HistoryState extends State { color: Theme.of(context) .colorScheme .primaryContainer - .withValues(alpha: 0.5), + .withValues(alpha: 0.7), shape: BoxShape.circle, ), child: Icon( @@ -425,7 +425,9 @@ class _HistoryState extends State { session['date']!, style: TextStyle( fontSize: 12, - color: Colors.grey[600], + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, fontWeight: FontWeight.w500, ), ), @@ -458,7 +460,9 @@ class _HistoryState extends State { overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 13, - color: Colors.grey[600], + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, ), ), const SizedBox(height: 4), @@ -466,7 +470,9 @@ class _HistoryState extends State { '${session['start']} - ${session['end']}', style: TextStyle( fontSize: 11, - color: Colors.grey[500], + color: Theme.of(context) + .colorScheme + .outline, ), ), ], diff --git a/lib/widgets/timer.dart b/lib/widgets/timer.dart index ffe34a3..bb45083 100644 --- a/lib/widgets/timer.dart +++ b/lib/widgets/timer.dart @@ -381,7 +381,9 @@ circle indicates an active session. } }, fontWeight: FontWeight.bold, - backgroundColor: Colors.lightGreenAccent.shade100, + backgroundColor: Theme.of(context).brightness == Brightness.dark + ? Colors.green.shade900 + : Colors.lightGreenAccent.shade100, ); final pauseButton = AppButton( @@ -400,7 +402,7 @@ of the Resume button. }, ); - // TODO 20240708 gjw COMMENT OUT BUTTONS UNTIL FUINCTIONALITY MIGRATED + // Note: 20240708 gjw COMMENT OUT BUTTONS UNTIL FUNCTIONALITY MIGRATED // // I originally had these extra two buttons but UX suggests one buttont to // PAUSE whcih when tapped becomes RESUME and if long held it is RESET. @@ -437,7 +439,9 @@ three dings. The blue progress circle indicates an active session. .trim(), onPressed: _intro, fontWeight: FontWeight.bold, - backgroundColor: Colors.blue.shade100, + backgroundColor: Theme.of(context).brightness == Brightness.dark + ? Colors.blue.shade900 + : Colors.blue.shade100, ); final guidedButton = AppButton( @@ -455,7 +459,9 @@ audio may take a little time to download for the Web version. .trim(), onPressed: _guided, fontWeight: FontWeight.bold, - backgroundColor: Colors.purple.shade100, + backgroundColor: Theme.of(context).brightness == Brightness.dark + ? Colors.purple.shade900 + : Colors.purple.shade100, ); //////////////////////////////////// @@ -469,7 +475,8 @@ audio may take a little time to download for the Web version. return ChoiceChip( label: Text(number.toString()), selected: _duration == number * 60, - selectedColor: Colors.lightGreenAccent, + selectedColor: + Theme.of(context).colorScheme.primary.withValues(alpha: 0.3), showCheckmark: false, // This will hide the tick mark. onSelected: (selected) { if (selected) { @@ -501,10 +508,16 @@ audio may take a little time to download for the Web version. final buttonsMatrix = Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.5), + color: Theme.of(context) + .colorScheme + .surfaceContainer + .withValues(alpha: 0.5), borderRadius: BorderRadius.circular(32), border: Border.all( - color: Colors.white.withValues(alpha: 0.5), + color: Theme.of(context) + .colorScheme + .outlineVariant + .withValues(alpha: 0.3), ), ), child: Column( @@ -555,7 +568,10 @@ audio may take a little time to download for the Web version. borderSide: BorderSide.none, ), filled: true, - fillColor: Colors.white.withValues(alpha: 0.8), + fillColor: Theme.of(context) + .colorScheme + .surfaceContainer + .withValues(alpha: 0.8), ), ), const SizedBox(height: 12), @@ -570,7 +586,10 @@ audio may take a little time to download for the Web version. borderSide: BorderSide.none, ), filled: true, - fillColor: Colors.white.withValues(alpha: 0.8), + fillColor: Theme.of(context) + .colorScheme + .surfaceContainer + .withValues(alpha: 0.8), ), maxLines: 2, ),