import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:journaler/db.dart'; import 'package:journaler/notes.dart'; import 'package:system_tray/system_tray.dart'; import 'package:window_manager/window_manager.dart'; import 'package:audioplayers/audioplayers.dart'; // TODO: Add an icon to the executable, simply use the existing tray icon // TODO: Add an entry field for the duration ie. the interval of apperance // TODO: Also the sound file, if possible... // TODO: Cram the above into the database const Duration popupInterval = Duration(minutes: 20); const String notificationSound = 'MeetTheSniper.mp3'; void main() async { await DB.init(); WidgetsFlutterBinding.ensureInitialized(); await windowManager.ensureInitialized(); WindowOptions windowOptions = const WindowOptions( size: Size(1600, 900), center: true, backgroundColor: Colors.transparent, skipTaskbar: false, titleBarStyle: TitleBarStyle.normal, ); windowManager.waitUntilReadyToShow(windowOptions, () async { await windowManager.hide(); }); runApp(const JournalerApp()); } class JournalerApp extends StatelessWidget { const JournalerApp({super.key}); static final TextTheme _baseTextTheme = const TextTheme( bodyMedium: TextStyle(fontSize: 24), ); static final TextStyle _baseTitleTextStyle = const TextStyle( fontSize: 20, fontWeight: FontWeight.w500, ); static final InputDecorationTheme _baseInputDecorationTheme = const InputDecorationTheme( border: OutlineInputBorder(), labelStyle: TextStyle(fontSize: 20), ); 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, ); 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), ), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.blue.shade300, width: 2.0), ), labelStyle: _baseInputDecorationTheme.labelStyle?.copyWith( color: Colors.grey[400], ), ), textTheme: _baseTextTheme.copyWith( bodyMedium: _baseTextTheme.bodyMedium?.copyWith(color: Colors.white), ), ); @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 Menu _menu = Menu(); final AudioPlayer _audioPlayer = AudioPlayer(); final TextEditingController _previousEntryController = TextEditingController(); final TextEditingController _currentEntryController = TextEditingController(); final FocusNode _currentEntryFocusNode = FocusNode(); final TextEditingController _todoController = TextEditingController(); Note? previousNote; Timer? _popupTimer; Timer? _debounceTimer; @override void initState() { super.initState(); windowManager.addListener(this); _initSystemTray(); _loadData(); _startPopupTimer(); windowManager.setPreventClose(true); _setWindowConfig(); } @override void dispose() { windowManager.removeListener(this); _popupTimer?.cancel(); _debounceTimer?.cancel(); _previousEntryController.dispose(); _currentEntryController.dispose(); _currentEntryFocusNode.dispose(); _todoController.dispose(); _audioPlayer.dispose(); super.dispose(); } @override void onWindowClose() { _saveData(); windowManager.hide(); } @override void onWindowFocus() { setState(() {}); } Future _initSystemTray() async { String iconPath = 'assets/app_icon.ico'; await _systemTray.initSystemTray(iconPath: iconPath, toolTip: "Journaler"); await _menu.buildFrom([ MenuItemLabel( label: 'Commit Sudoku', onClicked: (menuItem) => windowManager.destroy(), ), ]); await _systemTray.setContextMenu(_menu); _systemTray.registerSystemTrayEventHandler((eventName) { debugPrint("System Tray Event: $eventName"); if (eventName == kSystemTrayEventClick) { _showWindow(); } else if (eventName == kSystemTrayEventRightClick) { _systemTray.popUpContextMenu(); } }); } void _startPopupTimer() { _popupTimer = Timer.periodic(popupInterval, (timer) { _showWindow(); }); } Future _showWindow() async { _loadData(); bool wasVisible = await windowManager.isVisible(); if (!wasVisible) { await windowManager.setSize(const Size(1600, 900)); await windowManager.center(); await windowManager.show(); await windowManager.focus(); _currentEntryFocusNode.requestFocus(); await _playSound(); } else { await windowManager.focus(); _currentEntryFocusNode.requestFocus(); } } Future _playSound() async { await _audioPlayer.stop(); await _audioPlayer.play(AssetSource('sounds/$notificationSound')); debugPrint("Played sound: $notificationSound"); } void _loadData() async { final note = await getLatestNote(); previousNote = note; _previousEntryController.text = note?.content ?? ""; final todo = await getLatestTodo(); _todoController.text = todo?.content ?? ""; _currentEntryController.text = ""; debugPrint("Data loaded (placeholder)."); } void _saveData() async { String previousEntry = _previousEntryController.text; String currentEntry = _currentEntryController.text; String todoList = _todoController.text; await createNote(currentEntry); await createTodo(todoList); previousNote!.content = previousEntry; await updateNote(previousNote!); debugPrint( "Saving data (placeholder)... Current Entry: [${currentEntry.length} chars], Todo: [${todoList.length} chars]", ); } Future _setWindowConfig() async { await windowManager.setAspectRatio(16 / 9); } @override Widget build(BuildContext context) { // Wrap Scaffold with RawKeyboardListener as workaround for Escape key return RawKeyboardListener( focusNode: FocusNode(), // Needs its own node onKey: (RawKeyEvent event) { if (event is RawKeyDownEvent && event.logicalKey == LogicalKeyboardKey.escape) { debugPrint( "Escape pressed inside MainPage (RawKeyboardListener - Workaround)", ); // Call method directly since we are in the state FocusManager.instance.primaryFocus?.unfocus(); // Keep unfocus attempt onWindowClose(); } }, child: Scaffold( appBar: AppBar(title: const Text('Journaler'), actions: const []), body: Padding( padding: const EdgeInsets.all(8.0), child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded( flex: 9, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded( child: TextField( controller: _previousEntryController, maxLines: null, expands: true, style: Theme.of(context).textTheme.bodyMedium, decoration: const InputDecoration( labelText: 'Previous Entry', ), ), ), const SizedBox(height: 8), Expanded( child: TextField( controller: _currentEntryController, focusNode: _currentEntryFocusNode, maxLines: null, expands: true, autofocus: true, style: Theme.of(context).textTheme.bodyMedium, decoration: const InputDecoration( labelText: 'Current Entry (What\'s on your mind?)', ), onChanged: (text) {}, ), ), ], ), ), const SizedBox(width: 8), Expanded( flex: 3, child: TextField( controller: _todoController, maxLines: null, expands: true, style: Theme.of( context, ).textTheme.bodyMedium, // Apply theme text style decoration: const InputDecoration( labelText: 'Todo', // border: OutlineInputBorder(), // Handled by theme // contentPadding: EdgeInsets.all(8.0), // Handled by theme or default ), ), ), ], ), ), ), ); } } // --- End Actions and Shortcuts ---