import 'dart:async'; import 'dart:io'; // Required for Platform check import 'package:flutter/material.dart'; 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 = 'notification.wav'; // Sound file in assets/sounds/ // -------------------- void main() async { WidgetsFlutterBinding.ensureInitialized(); // Must add this line. await windowManager.ensureInitialized(); // Use it only after calling `hiddenWindowAtLaunch` WindowOptions windowOptions = const WindowOptions( size: Size(800, 600), 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}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Journaler', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: const MainPage(), debugShowCheckedModeBanner: false, ); } } class MainPage extends StatefulWidget { const MainPage({super.key}); @override State createState() => _MainPageState(); } class _MainPageState extends State with WindowListener { // final AppWindow _appWindow = AppWindow(); // REMOVED - Not used final SystemTray _systemTray = SystemTray(); final AudioPlayer _audioPlayer = AudioPlayer(); final TextEditingController _previousEntryController = TextEditingController(); final TextEditingController _currentEntryController = TextEditingController(); final TextEditingController _todoController = TextEditingController(); Timer? _popupTimer; @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); } @override void dispose() { windowManager.removeListener(this); _popupTimer?.cancel(); _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 = Platform.isWindows ? 'assets/app_icon.ico' : 'assets/app_icon.png'; // 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"); if (eventName == kSystemTrayEventClick) { _showWindow(); } else if (eventName == kSystemTrayEventRightClick) { // Optional: Show a context menu on right click // _systemTray.popUpContextMenu() - need to define menu items first } }); } // --- Periodic Popup & Sound --- // void _startPopupTimer() { _popupTimer = Timer.periodic(popupInterval, (timer) { _showWindowAndPlaySound(); }); } Future _showWindowAndPlaySound() async { await _showWindow(); await _playSound(); } Future _showWindow() async { bool isVisible = await windowManager.isVisible(); if (!isVisible) { await windowManager.show(); await windowManager.focus(); } } Future _playSound() async { try { // Assumes the sound file is in assets/sounds/ await _audioPlayer.play(AssetSource('sounds/$notificationSound')); debugPrint("Played sound: $notificationSound"); } catch (e) { debugPrint("Error playing sound: $e"); // 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 } // --- UI Build --- // @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Journaler'), // Optional: Add a save button or other actions actions: [ IconButton( icon: const Icon(Icons.save), onPressed: _saveData, tooltip: 'Save Current Entry & Todo', ), IconButton( icon: const Icon(Icons.volume_up), onPressed: _playSound, // For testing sound tooltip: 'Test Sound', ), IconButton( icon: const Icon(Icons.timer), onPressed: _showWindowAndPlaySound, // For testing popup tooltip: 'Test Popup', ), ], ), 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 readOnly: true, // Make it non-editable decoration: const InputDecoration( labelText: 'Previous Entry', border: OutlineInputBorder(), contentPadding: EdgeInsets.all(8.0), ), ), ), const SizedBox(height: 8), // Spacing // Current Entry Expanded( child: TextField( controller: _currentEntryController, maxLines: null, expands: true, autofocus: true, // Focus here when window appears decoration: const InputDecoration( labelText: 'Current Entry (What\'s on your mind?)', border: OutlineInputBorder(), contentPadding: EdgeInsets.all(8.0), ), 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, decoration: const InputDecoration( labelText: 'Todo List', border: OutlineInputBorder(), contentPadding: EdgeInsets.all(8.0), ), onChanged: (text) { // Optional: Auto-save Todo list changes // Consider debouncing if saving frequently }, ), ), ], ), ), ); } } // 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(); // } // }