diff --git a/lib/main.dart b/lib/main.dart index 7b7f5b6..f290592 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,122 +1,332 @@ +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 -void main() { - runApp(const MyApp()); +// --- 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 MyApp extends StatelessWidget { - const MyApp({super.key}); +class JournalerApp extends StatelessWidget { + const JournalerApp({super.key}); - // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( - title: 'Flutter Demo', + title: 'Journaler', theme: ThemeData( - // This is the theme of your application. - // - // TRY THIS: Try running your application with "flutter run". You'll see - // the application has a purple toolbar. Then, without quitting the app, - // try changing the seedColor in the colorScheme below to Colors.green - // and then invoke "hot reload" (save your changes or press the "hot - // reload" button in a Flutter-supported IDE, or press "r" if you used - // the command line to start the app). - // - // Notice that the counter didn't reset back to zero; the application - // state is not lost during the reload. To reset the state, use hot - // restart instead. - // - // This works for code too, not just values: Most code changes can be - // tested with just a hot reload. - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + primarySwatch: Colors.blue, + visualDensity: VisualDensity.adaptivePlatformDensity, ), - home: const MyHomePage(title: 'Flutter Demo Home Page'), + home: const MainPage(), + debugShowCheckedModeBanner: false, ); } } -class MyHomePage extends StatefulWidget { - const MyHomePage({super.key, required this.title}); - - // This widget is the home page of your application. It is stateful, meaning - // that it has a State object (defined below) that contains fields that affect - // how it looks. - - // This class is the configuration for the state. It holds the values (in this - // case the title) provided by the parent (in this case the App widget) and - // used by the build method of the State. Fields in a Widget subclass are - // always marked "final". - - final String title; +class MainPage extends StatefulWidget { + const MainPage({super.key}); @override - State createState() => _MyHomePageState(); + State createState() => _MainPageState(); } -class _MyHomePageState extends State { - int _counter = 0; +class _MainPageState extends State with WindowListener { + final AppWindow _appWindow = AppWindow(); + final SystemTray _systemTray = SystemTray(); + final AudioPlayer _audioPlayer = AudioPlayer(); - void _incrementCounter() { - setState(() { - // This call to setState tells the Flutter framework that something has - // changed in this State, which causes it to rerun the build method below - // so that the display can reflect the updated values. If we changed - // _counter without calling setState(), then the build method would not be - // called again, and so nothing would appear to happen. - _counter++; - }); + 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 + // Example: + // await saveEntryToDatabase(_currentEntryController.text); + // await saveTodoListToFile(_todoController.text); + + // You might want to move the '_currentEntryController.text' to become + // the '_previousEntryController.text' for the *next* session here or upon loading. + + debugPrint( + "Data saved (placeholder). Previous: [...], Current: [${_currentEntryController.text.length} chars], Todo: [${_todoController.text.length} chars]", + ); + // Potentially clear current entry after saving, or handle it on next load + // _currentEntryController.clear(); + } + + // --- UI Build --- // @override Widget build(BuildContext context) { - // This method is rerun every time setState is called, for instance as done - // by the _incrementCounter method above. - // - // The Flutter framework has been optimized to make rerunning build methods - // fast, so that you can just rebuild anything that needs updating rather - // than having to individually change instances of widgets. return Scaffold( appBar: AppBar( - // TRY THIS: Try changing the color here to a specific color (to - // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar - // change color while the other colors stay the same. - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - // Here we take the value from the MyHomePage object that was created by - // the App.build method, and use it to set our appbar title. - title: Text(widget.title), + 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: Center( - // Center is a layout widget. It takes a single child and positions it - // in the middle of the parent. - child: Column( - // Column is also a layout widget. It takes a list of children and - // arranges them vertically. By default, it sizes itself to fit its - // children horizontally, and tries to be as tall as its parent. - // - // Column has various properties to control how it sizes itself and - // how it positions its children. Here we use mainAxisAlignment to - // center the children vertically; the main axis here is the vertical - // axis because Columns are vertical (the cross axis would be - // horizontal). - // - // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint" - // action in the IDE, or press "p" in the console), to see the - // wireframe for each widget. - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('You have pushed the button this many times:'), - Text( - '$_counter', - style: Theme.of(context).textTheme.headlineMedium, + 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 + }, + ), ), ], ), ), - floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: const Icon(Icons.add), - ), // This trailing comma makes auto-formatting nicer for build methods. ); } } + +// 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(); + } +}