import 'dart:async'; import 'dart:io'; // Required for Platform check import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; // Needed for LogicalKeyboardKey import 'package:system_tray/system_tray.dart'; import 'package:window_manager/window_manager.dart'; import 'package:audioplayers/audioplayers.dart'; // For path joining // --- Configuration --- const Duration popupInterval = Duration(hours: 1); // How often to pop up const String notificationSound = 'MeetTheSniper.mp3'; // -------------------- void main() async { WidgetsFlutterBinding.ensureInitialized(); // Must add this line. await windowManager.ensureInitialized(); // Use it only after calling `hiddenWindowAtLaunch` WindowOptions windowOptions = const WindowOptions( size: Size(1600, 900), // Increased size to 1600x900 center: true, backgroundColor: Colors.transparent, skipTaskbar: false, titleBarStyle: TitleBarStyle.normal, ); // Hide window at launch windowManager.waitUntilReadyToShow(windowOptions, () async { await windowManager.hide(); // Start hidden }); runApp(const JournalerApp()); } class JournalerApp extends StatelessWidget { const JournalerApp({super.key}); // --- Base Theme Configuration --- static final TextTheme _baseTextTheme = const TextTheme( bodyMedium: TextStyle(fontSize: 24), // Define other common styles if needed ); static final TextStyle _baseTitleTextStyle = const TextStyle( fontSize: 20, fontWeight: FontWeight.w500, ); static final InputDecorationTheme _baseInputDecorationTheme = const InputDecorationTheme( border: OutlineInputBorder(), labelStyle: TextStyle(fontSize: 20), // Base label size // contentPadding: EdgeInsets.all(12.0), // Example: Add padding globally ); // ------------------------------ // Define the light theme static final ThemeData lightTheme = ThemeData.light().copyWith( visualDensity: VisualDensity.adaptivePlatformDensity, colorScheme: ColorScheme.fromSeed( seedColor: Colors.blue, brightness: Brightness.light, ), scaffoldBackgroundColor: Colors.grey[100], appBarTheme: AppBarTheme( backgroundColor: Colors.blue, foregroundColor: Colors.white, titleTextStyle: _baseTitleTextStyle.copyWith(color: Colors.white), ), inputDecorationTheme: _baseInputDecorationTheme.copyWith( focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.blue.shade600, width: 2.0), ), labelStyle: _baseInputDecorationTheme.labelStyle?.copyWith( color: Colors.grey[700], ), ), textTheme: _baseTextTheme.copyWith( // Optionally override specific light theme text styles here ), ); // Define the dark theme static final ThemeData darkTheme = ThemeData.dark().copyWith( visualDensity: VisualDensity.adaptivePlatformDensity, colorScheme: ColorScheme.fromSeed( seedColor: Colors.blue, brightness: Brightness.dark, ), scaffoldBackgroundColor: Colors.grey[900], appBarTheme: AppBarTheme( backgroundColor: Colors.grey[850], foregroundColor: Colors.white, titleTextStyle: _baseTitleTextStyle.copyWith(color: Colors.white), ), inputDecorationTheme: _baseInputDecorationTheme.copyWith( border: const OutlineInputBorder( borderSide: BorderSide(color: Colors.grey), // Dark theme default border ), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.blue.shade300, width: 2.0), ), labelStyle: _baseInputDecorationTheme.labelStyle?.copyWith( color: Colors.grey[400], ), ), textTheme: _baseTextTheme.copyWith( // Ensure body text is readable on dark background bodyMedium: _baseTextTheme.bodyMedium?.copyWith(color: Colors.white), // Optionally override other dark theme text styles here ), ); @override Widget build(BuildContext context) { return MaterialApp( title: 'Journaler', theme: lightTheme, darkTheme: darkTheme, themeMode: ThemeMode.system, home: const MainPage(), debugShowCheckedModeBanner: false, ); } } class MainPage extends StatefulWidget { const MainPage({super.key}); @override State createState() => _MainPageState(); } class _MainPageState extends State with WindowListener { final SystemTray _systemTray = SystemTray(); final AudioPlayer _audioPlayer = AudioPlayer(); final TextEditingController _previousEntryController = TextEditingController(); final TextEditingController _currentEntryController = TextEditingController(); final TextEditingController _todoController = TextEditingController(); Timer? _popupTimer; Timer? _debounceTimer; // Timer for debouncing Todo saves @override void initState() { super.initState(); windowManager.addListener(this); _initSystemTray(); _loadData(); // Placeholder for loading initial data _startPopupTimer(); // Prevent window from closing, instead hide it // Needs `implements WindowListener` and `windowManager.addListener(this);` // and `windowManager.removeListener(this);` in dispose windowManager.setPreventClose(true); // Set aspect ratio after ensuring initialized _setWindowConfig(); } @override void dispose() { windowManager.removeListener(this); _popupTimer?.cancel(); _debounceTimer?.cancel(); // Cancel debounce timer on dispose _previousEntryController.dispose(); _currentEntryController.dispose(); _todoController.dispose(); _audioPlayer.dispose(); super.dispose(); } // --- Window Listener --- // @override void onWindowClose() { _saveData(); // Placeholder for saving data windowManager.hide(); // Hide instead of closing // Alternatively, to fully close: // windowManager.destroy(); } @override void onWindowFocus() { // Maybe reload data if needed when window becomes active setState(() { // If you need to refresh state }); } // --- System Tray --- // Future _initSystemTray() async { // Use simple relative paths for assets declared in pubspec.yaml // The build process handles packaging these assets correctly. String iconPath = 'assets/app_icon.ico'; // IMPORTANT: You need to create an icon file named app_icon.ico (for Windows) // and/or app_icon.png (for Mac/Linux) and place it in the 'assets/' directory. // Create the 'assets' directory if it doesn't exist at the root of your project. // Then, ensure your pubspec.yaml lists the 'assets/' directory: // flutter: // uses-material-design: true // assets: // - assets/ // - assets/sounds/ # Keep this if you have it // Check if the asset file exists before trying to use it (optional but recommended) // Note: This basic check works during development. Accessing assets might // require different handling in production builds depending on the platform. // try { // await rootBundle.load(iconPath); // Check if asset is loadable // } catch (_) { // debugPrint("Error: System tray icon '$iconPath' not found in assets."); // // Handle the error, maybe use a default icon or skip tray init // return; // } await _systemTray.initSystemTray( // title: "Journaler", // Optional: Title shown on hover (might not work on all OS) iconPath: iconPath, // Use the corrected path toolTip: "Journaler", ); // Handle system tray menu item clicks _systemTray.registerSystemTrayEventHandler((eventName) { debugPrint("System Tray Event: $eventName"); _showWindow(); }); } // --- Periodic Popup & Sound --- // void _startPopupTimer() { _popupTimer = Timer.periodic(popupInterval, (timer) { // Timer now only requests the window to show _showWindow(); }); } Future _showWindow() async { bool wasVisible = await windowManager.isVisible(); if (!wasVisible) { // Ensure size and position before showing await windowManager.setSize(const Size(1600, 900)); // Set desired size await windowManager.center(); // Center the window await windowManager.show(); await windowManager.focus(); // Play sound ONLY when window becomes visible await _playSound(); } else { // If already visible, just bring to front and focus await windowManager.focus(); } } Future _playSound() async { try { // Stop any previous playback before starting anew await _audioPlayer.stop(); // Assumes the sound file is in assets/sounds/ await _audioPlayer.play(AssetSource('sounds/$notificationSound')); debugPrint("Played sound: $notificationSound"); } catch (e, stackTrace) { // Catch stack trace for more details debugPrint("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); debugPrint("Error playing sound '$notificationSound': $e"); debugPrint("Stack trace: $stackTrace"); debugPrint( "Ensure file exists, is valid audio, and assets/sounds/ is in pubspec.yaml", ); debugPrint("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); // Handle error, e.g., show a notification or log } } // --- Data Handling (Placeholders) --- // void _loadData() { // TODO: Implement logic to load previous entry and todo list // Example: // _previousEntryController.text = await loadPreviousEntryFromDatabase(); // _todoController.text = await loadTodoListFromFile(); _previousEntryController.text = "This is a placeholder for the previous entry."; _todoController.text = "- Placeholder Todo 1\n- Placeholder Todo 2"; _currentEntryController.text = ""; // Current entry always starts empty debugPrint("Data loaded (placeholder)."); } void _saveData() { // TODO: Implement logic to save the current entry and todo list // This is called when the window is closed (hidden) or the save button is pressed. String currentEntry = _currentEntryController.text; String todoList = _todoController.text; print( "Saving data (placeholder)... Current Entry: [${currentEntry.length} chars], Todo: [${todoList.length} chars]", ); // --- Your persistence logic goes here --- // Example: // await saveEntryToDatabase(currentEntry); // await saveTodoListToFile(todoList); // --------------------------------------- // You might want to update the previous entry for the *next* session here, // or handle this logic when loading data next time. // Example (simplistic, assumes immediate update for next view): // setState(() { // _previousEntryController.text = currentEntry; // _currentEntryController.clear(); // Clear current entry after saving // }); // Potentially clear current entry after saving, or handle it on next load // _currentEntryController.clear(); // Decide if you want to clear it immediately } // --- Add a specific save function for Todo List --- // void _saveTodoList() { // Cancel any existing timer if (_debounceTimer?.isActive ?? false) _debounceTimer!.cancel(); // Start a new timer _debounceTimer = Timer(const Duration(milliseconds: 500), () { // This code runs after 500ms of inactivity String todoList = _todoController.text; print("Debounced Save: Saving Todo list... [${todoList.length} chars]"); // --- Your actual todo list persistence logic goes here --- // Example: // await saveTodoListToFile(todoList); // -------------------------------------------------------- }); } // Helper to set initial window config like aspect ratio Future _setWindowConfig() async { // Wait a moment to ensure window manager is fully ready after init // Might not be strictly necessary but can prevent race conditions // await Future.delayed(const Duration(milliseconds: 100)); await windowManager.setAspectRatio(16 / 9); // Optionally set min/max size if desired // await windowManager.setMinimumSize(const Size(800, 450)); } // --- UI Build --- // @override Widget build(BuildContext context) { // Listen for keyboard events to close on Escape return RawKeyboardListener( focusNode: FocusNode(), // Need a focus node to receive keys autofocus: true, // Ensure it can receive focus onKey: (event) { if (event.logicalKey == LogicalKeyboardKey.escape) { debugPrint("Escape key pressed - hiding window."); windowManager.hide(); } }, child: Scaffold( appBar: AppBar( title: const Text('Journaler'), actions: const [ // Remove all buttons // No actions needed anymore ], ), body: Padding( padding: const EdgeInsets.all(8.0), child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Main journal area (8-10 columns) Expanded( flex: 9, // Adjust flex factor (e.g., 8, 9, 10) for desired width ratio child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Previous Entry (read-only conceptually, but TextField for easy display) Expanded( child: TextField( controller: _previousEntryController, maxLines: null, // Allows unlimited lines expands: true, // Fills the available space style: Theme.of( context, ).textTheme.bodyMedium, // Apply theme text style decoration: const InputDecoration( labelText: 'Previous Entry', // border: OutlineInputBorder(), // Handled by theme // contentPadding: EdgeInsets.all(8.0), // Handled by theme or default ), ), ), const SizedBox(height: 8), // Spacing // Current Entry Expanded( child: TextField( controller: _currentEntryController, maxLines: null, expands: true, autofocus: true, // Focus here when window appears style: Theme.of( context, ).textTheme.bodyMedium, // Apply theme text style decoration: const InputDecoration( labelText: 'Current Entry (What\'s on your mind?)', // border: OutlineInputBorder(), // Handled by theme // contentPadding: EdgeInsets.all(8.0), // Handled by theme or default ), onChanged: (text) { // Optional: Add auto-save logic here if desired }, ), ), ], ), ), const SizedBox(width: 8), // Spacing between columns // Todo List area (remaining columns) Expanded( flex: 3, // Adjust flex factor (12 - 9 = 3, or 12 - 8 = 4, etc.) child: TextField( controller: _todoController, maxLines: null, expands: true, style: Theme.of( context, ).textTheme.bodyMedium, // Apply theme text style decoration: const InputDecoration( labelText: 'Todo List', // border: OutlineInputBorder(), // Handled by theme // contentPadding: EdgeInsets.all(8.0), // Handled by theme or default ), onChanged: (text) { // Auto-save Todo list changes (consider debouncing) _saveTodoList(); }, ), ), ], ), ), ), ); } } // REMOVED - Helper class for AppWindow interactions (optional, but good practice) // class AppWindow { // Future init() async { // // Can add more window setup here if needed // } // // Future show() async { // await windowManager.show(); // await windowManager.focus(); // } // // Future hide() async { // await windowManager.hide(); // } // }