1143 lines
		
	
	
		
			39 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			1143 lines
		
	
	
		
			39 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
import 'dart:async';
 | 
						|
import 'dart:io';
 | 
						|
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';
 | 
						|
import 'package:flutter/gestures.dart';
 | 
						|
import 'dart:math';
 | 
						|
import 'package:path/path.dart' as path;
 | 
						|
import 'package:ps_list/ps_list.dart';
 | 
						|
 | 
						|
// TODO: Sound does not play when ran from a different workdir? Weird
 | 
						|
// TODO: Fix saving the same scratch over and over again
 | 
						|
 | 
						|
// Default values - will be replaced by DB values if they exist
 | 
						|
const Duration _defaultPopupInterval = Duration(minutes: 20);
 | 
						|
const String _defaultNotificationSound = 'MeetTheSniper.mp3';
 | 
						|
 | 
						|
// Flag to indicate if there's a pending show request from a secondary instance
 | 
						|
bool _pendingShowRequest = false;
 | 
						|
 | 
						|
// Global navigator key to access the navigator state from anywhere
 | 
						|
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
 | 
						|
 | 
						|
/// Paths for IPC file in system temp directory
 | 
						|
class AppFiles {
 | 
						|
  static String get ipcFilePath =>
 | 
						|
      path.join(Directory.systemTemp.path, 'journaler_ipc.txt');
 | 
						|
}
 | 
						|
 | 
						|
void main() async {
 | 
						|
  WidgetsFlutterBinding.ensureInitialized();
 | 
						|
 | 
						|
  final ipcFile = File(AppFiles.ipcFilePath);
 | 
						|
 | 
						|
  if (await alreadyRunning()) {
 | 
						|
    await signalPrimaryInstanceAndExit(ipcFile);
 | 
						|
  } else {
 | 
						|
    await runPrimaryInstance(ipcFile);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
Future<bool> alreadyRunning() async {
 | 
						|
  final processes = await PSList.getRunningProcesses();
 | 
						|
  final executable = Platform.resolvedExecutable;
 | 
						|
  final executableName = path.basename(executable);
 | 
						|
  final journalers =
 | 
						|
      processes.where((process) => process == executableName).toList();
 | 
						|
  debugPrint("Journalers: $journalers");
 | 
						|
  return journalers.length > 1;
 | 
						|
}
 | 
						|
 | 
						|
/// Run the primary instance of the application
 | 
						|
Future<void> runPrimaryInstance(File ipcFile) async {
 | 
						|
  debugPrint("Starting as primary instance");
 | 
						|
 | 
						|
  // Create or clear the IPC file
 | 
						|
  if (!await ipcFile.exists()) {
 | 
						|
    await ipcFile.create();
 | 
						|
  }
 | 
						|
  // Start a watcher for the IPC file to detect signals from other instances
 | 
						|
  startIpcWatcher(ipcFile);
 | 
						|
 | 
						|
  // Initialize the app
 | 
						|
  await DB.init();
 | 
						|
  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());
 | 
						|
 | 
						|
  // Set up termination handlers to clean up resources
 | 
						|
  setupTerminationHandlers(ipcFile);
 | 
						|
}
 | 
						|
 | 
						|
/// Handle termination signals by cleaning up IPC file
 | 
						|
void setupTerminationHandlers(File ipcFile) {
 | 
						|
  // Handle normal termination
 | 
						|
  ProcessSignal.sigterm.watch().listen((_) async {
 | 
						|
    await cleanupFiles(ipcFile);
 | 
						|
    exit(0);
 | 
						|
  });
 | 
						|
 | 
						|
  // Handle Windows and Linux CTRL+C termination
 | 
						|
  if (Platform.isWindows || Platform.isLinux) {
 | 
						|
    ProcessSignal.sigint.watch().listen((_) async {
 | 
						|
      await cleanupFiles(ipcFile);
 | 
						|
      exit(0);
 | 
						|
    });
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
/// Clean up IPC file
 | 
						|
Future<void> cleanupFiles(File ipcFile) async {
 | 
						|
  try {
 | 
						|
    if (await ipcFile.exists()) {
 | 
						|
      await ipcFile.delete();
 | 
						|
    }
 | 
						|
    debugPrint("Removed IPC file on app termination");
 | 
						|
  } catch (e) {
 | 
						|
    debugPrint("Error cleaning up files: $e");
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
/// Signal the primary instance to show itself, then exit
 | 
						|
Future<void> signalPrimaryInstanceAndExit(File ipcFile) async {
 | 
						|
  debugPrint(
 | 
						|
    "Another instance is already running. Signaling it to show window.",
 | 
						|
  );
 | 
						|
 | 
						|
  try {
 | 
						|
    // Create the IPC file if it doesn't exist
 | 
						|
    if (!await ipcFile.exists()) {
 | 
						|
      await ipcFile.create();
 | 
						|
    }
 | 
						|
 | 
						|
    // Write a 'show' command with a timestamp
 | 
						|
    final timestamp = DateTime.now().millisecondsSinceEpoch;
 | 
						|
    await ipcFile.writeAsString('show:$timestamp');
 | 
						|
    debugPrint("Signal sent to primary instance via IPC file");
 | 
						|
 | 
						|
    // Give the primary instance a moment to process the signal
 | 
						|
    await Future.delayed(const Duration(seconds: 2));
 | 
						|
  } catch (e) {
 | 
						|
    debugPrint("Error signaling primary instance: $e");
 | 
						|
  }
 | 
						|
 | 
						|
  // Exit this instance
 | 
						|
  debugPrint("Exiting secondary instance");
 | 
						|
  exit(0);
 | 
						|
}
 | 
						|
 | 
						|
/// Start watching the IPC file for signals from other instances
 | 
						|
void startIpcWatcher(File ipcFile) {
 | 
						|
  // Initial check of the file content
 | 
						|
  checkIpcFileContent(ipcFile);
 | 
						|
 | 
						|
  // Set up a file watcher
 | 
						|
  final watcher = ipcFile.watch();
 | 
						|
  watcher.listen((event) {
 | 
						|
    if (event.type == FileSystemEvent.modify) {
 | 
						|
      checkIpcFileContent(ipcFile);
 | 
						|
    }
 | 
						|
  });
 | 
						|
 | 
						|
  // Also set up a periodic check as a fallback
 | 
						|
  Timer.periodic(const Duration(milliseconds: 200), (_) {
 | 
						|
    checkIpcFileContent(ipcFile);
 | 
						|
  });
 | 
						|
 | 
						|
  debugPrint("IPC file watcher started");
 | 
						|
}
 | 
						|
 | 
						|
/// Check the IPC file content for commands
 | 
						|
Future<void> checkIpcFileContent(File ipcFile) async {
 | 
						|
  try {
 | 
						|
    if (await ipcFile.exists()) {
 | 
						|
      final content = await ipcFile.readAsString();
 | 
						|
 | 
						|
      if (content.isNotEmpty && content.startsWith('show:')) {
 | 
						|
        // Clear the file immediately to avoid processing the same command multiple times
 | 
						|
        await ipcFile.writeAsString('');
 | 
						|
 | 
						|
        // Process the show command
 | 
						|
        if (MainPageState.instance != null) {
 | 
						|
          debugPrint("Received show command - making window visible");
 | 
						|
          MainPageState.instance!.showWindow();
 | 
						|
        } else {
 | 
						|
          debugPrint(
 | 
						|
            "MainPageState not initialized yet - setting pending flag",
 | 
						|
          );
 | 
						|
          _pendingShowRequest = true;
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
  } catch (e) {
 | 
						|
    debugPrint("Error processing IPC file: $e");
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
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,
 | 
						|
      navigatorKey: navigatorKey,
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class MainPage extends StatefulWidget {
 | 
						|
  const MainPage({super.key});
 | 
						|
 | 
						|
  @override
 | 
						|
  State<MainPage> createState() => MainPageState();
 | 
						|
}
 | 
						|
 | 
						|
class MainPageState extends State<MainPage> with WindowListener {
 | 
						|
  // Static reference to the current instance
 | 
						|
  static MainPageState? instance;
 | 
						|
 | 
						|
  final SystemTray _systemTray = SystemTray();
 | 
						|
  final Menu _menu = Menu();
 | 
						|
  final AudioPlayer _audioPlayer = AudioPlayer();
 | 
						|
  double _volume = 0.7; // Default volume level (0.0 to 1.0)
 | 
						|
 | 
						|
  final TextEditingController _previousEntryController =
 | 
						|
      TextEditingController();
 | 
						|
  final TextEditingController _currentEntryController = TextEditingController();
 | 
						|
  final FocusNode _currentEntryFocusNode = FocusNode();
 | 
						|
  final TextEditingController _scratchController = TextEditingController();
 | 
						|
  final TextEditingController _intervalController = TextEditingController();
 | 
						|
  final TextEditingController _soundController = TextEditingController();
 | 
						|
  final TextEditingController _searchController = TextEditingController();
 | 
						|
 | 
						|
  Note? previousNote;
 | 
						|
  Note? _currentlyDisplayedNote;
 | 
						|
  Duration _currentPopupInterval = _defaultPopupInterval;
 | 
						|
  String _currentNotificationSound = _defaultNotificationSound;
 | 
						|
 | 
						|
  bool _canGoPrevious = false;
 | 
						|
  bool _canGoNext = false;
 | 
						|
  bool _isSearching = false;
 | 
						|
  List<Note> _searchResults = [];
 | 
						|
 | 
						|
  Timer? _popupTimer;
 | 
						|
  Timer? _debounceTimer;
 | 
						|
  Timer? _searchDebounceTimer;
 | 
						|
 | 
						|
  @override
 | 
						|
  void initState() {
 | 
						|
    super.initState();
 | 
						|
    // Store reference to this instance
 | 
						|
    instance = this;
 | 
						|
    windowManager.addListener(this);
 | 
						|
    _initSystemTray();
 | 
						|
    _loadData();
 | 
						|
    _loadVolume();
 | 
						|
    windowManager.setPreventClose(true);
 | 
						|
    _setWindowConfig();
 | 
						|
 | 
						|
    // Check if there's a pending show request from another instance
 | 
						|
    WidgetsBinding.instance.addPostFrameCallback((_) {
 | 
						|
      if (_pendingShowRequest) {
 | 
						|
        debugPrint("Processing pending show request during initialization");
 | 
						|
        _pendingShowRequest = false;
 | 
						|
        showWindow();
 | 
						|
      }
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  @override
 | 
						|
  void dispose() {
 | 
						|
    // Clear reference to this instance
 | 
						|
    if (instance == this) {
 | 
						|
      instance = null;
 | 
						|
    }
 | 
						|
    windowManager.removeListener(this);
 | 
						|
    _popupTimer?.cancel();
 | 
						|
    _debounceTimer?.cancel();
 | 
						|
    _searchDebounceTimer?.cancel();
 | 
						|
    _previousEntryController.dispose();
 | 
						|
    _currentEntryController.dispose();
 | 
						|
    _currentEntryFocusNode.dispose();
 | 
						|
    _scratchController.dispose();
 | 
						|
    _intervalController.dispose();
 | 
						|
    _soundController.dispose();
 | 
						|
    _searchController.dispose();
 | 
						|
    _audioPlayer.dispose();
 | 
						|
    super.dispose();
 | 
						|
  }
 | 
						|
 | 
						|
  @override
 | 
						|
  void onWindowClose() async {
 | 
						|
    // Save data when window is closed
 | 
						|
    await _saveData();
 | 
						|
    windowManager.hide();
 | 
						|
  }
 | 
						|
 | 
						|
  @override
 | 
						|
  void onWindowFocus() {
 | 
						|
    setState(() {});
 | 
						|
  }
 | 
						|
 | 
						|
  Future<void> _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?.cancel();
 | 
						|
    _popupTimer = Timer.periodic(_currentPopupInterval, (timer) {
 | 
						|
      showWindow();
 | 
						|
    });
 | 
						|
    debugPrint(
 | 
						|
      "Popup timer started with interval: ${_currentPopupInterval.inMinutes} minutes",
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  Future<void> showWindow() async {
 | 
						|
    _loadData();
 | 
						|
    try {
 | 
						|
      bool wasVisible = await windowManager.isVisible();
 | 
						|
      debugPrint("Current window visibility: $wasVisible");
 | 
						|
 | 
						|
      if (!wasVisible) {
 | 
						|
        // First make sure the window has the right size and position
 | 
						|
        await windowManager.setSize(const Size(1600, 900));
 | 
						|
        await windowManager.center();
 | 
						|
 | 
						|
        // Now show and focus
 | 
						|
        await windowManager.show();
 | 
						|
        await Future.delayed(const Duration(milliseconds: 100)); // Short delay
 | 
						|
        await windowManager.focus();
 | 
						|
 | 
						|
        // Set input focus
 | 
						|
        _currentEntryFocusNode.requestFocus();
 | 
						|
 | 
						|
        // Play notification sound
 | 
						|
        await _playSound();
 | 
						|
 | 
						|
        debugPrint("Window made visible and focused");
 | 
						|
      } else {
 | 
						|
        // Already visible, just focus
 | 
						|
        await windowManager.focus();
 | 
						|
        _currentEntryFocusNode.requestFocus();
 | 
						|
        debugPrint("Window already visible, just focused");
 | 
						|
      }
 | 
						|
 | 
						|
      // Verify the window is now visible
 | 
						|
      bool isNowVisible = await windowManager.isVisible();
 | 
						|
      debugPrint("Window visibility after show attempt: $isNowVisible");
 | 
						|
 | 
						|
      if (!isNowVisible) {
 | 
						|
        debugPrint(
 | 
						|
          "WARNING: Window still not visible after show attempt, trying again",
 | 
						|
        );
 | 
						|
        await windowManager.show();
 | 
						|
        await windowManager.focus();
 | 
						|
      }
 | 
						|
    } catch (e) {
 | 
						|
      debugPrint("Error showing window: $e");
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  // Convert linear slider value (0.0-1.0) to logarithmic volume (for better human hearing perception)
 | 
						|
  double _linearToLogVolume(double linearValue) {
 | 
						|
    // Prevent log(0) which is -infinity
 | 
						|
    if (linearValue <= 0.01) return 0.0;
 | 
						|
 | 
						|
    // This is a common audio perception formula based on the Weber-Fechner law
 | 
						|
    // Using a custom curve that gives good control at low volumes (where human hearing is most sensitive)
 | 
						|
    return pow(linearValue, 2).toDouble();
 | 
						|
  }
 | 
						|
 | 
						|
  Future<void> _playSound() async {
 | 
						|
    await _audioPlayer.stop();
 | 
						|
    try {
 | 
						|
      // Set volume before playing (convert linear slider value to log volume)
 | 
						|
      await _audioPlayer.setVolume(_linearToLogVolume(_volume));
 | 
						|
      await _audioPlayer.play(AssetSource('sounds/$_currentNotificationSound'));
 | 
						|
      debugPrint(
 | 
						|
        "Played sound: $_currentNotificationSound at volume: $_volume (log: ${_linearToLogVolume(_volume)})",
 | 
						|
      );
 | 
						|
    } catch (e) {
 | 
						|
      debugPrint("Error playing sound $_currentNotificationSound: $e");
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  Future<void> _checkNavigation() async {
 | 
						|
    if (_currentlyDisplayedNote == null) {
 | 
						|
      setState(() {
 | 
						|
        _canGoPrevious = false;
 | 
						|
        _canGoNext = false;
 | 
						|
      });
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    final prev = await getPreviousNote(_currentlyDisplayedNote!.date);
 | 
						|
    final bool isLatest = _currentlyDisplayedNote!.date == previousNote?.date;
 | 
						|
 | 
						|
    setState(() {
 | 
						|
      _canGoPrevious = prev != null;
 | 
						|
      _canGoNext = !isLatest;
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  Future<void> _goToPreviousNote() async {
 | 
						|
    if (!_canGoPrevious || _currentlyDisplayedNote == null) return;
 | 
						|
 | 
						|
    // Save the current note content before navigating away
 | 
						|
    if (_currentlyDisplayedNote != null) {
 | 
						|
      _currentlyDisplayedNote!.content = _previousEntryController.text;
 | 
						|
      await updateNote(_currentlyDisplayedNote!);
 | 
						|
    }
 | 
						|
 | 
						|
    final prevNote = await getPreviousNote(_currentlyDisplayedNote!.date);
 | 
						|
    if (prevNote != null) {
 | 
						|
      setState(() {
 | 
						|
        _currentlyDisplayedNote = prevNote;
 | 
						|
        _previousEntryController.text = prevNote.content;
 | 
						|
      });
 | 
						|
      await _checkNavigation();
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  Future<void> _goToNextNote() async {
 | 
						|
    if (!_canGoNext || _currentlyDisplayedNote == null) return;
 | 
						|
 | 
						|
    // Save the current note content before navigating away
 | 
						|
    if (_currentlyDisplayedNote != null) {
 | 
						|
      _currentlyDisplayedNote!.content = _previousEntryController.text;
 | 
						|
      await updateNote(_currentlyDisplayedNote!);
 | 
						|
    }
 | 
						|
 | 
						|
    final nextNote = await getNextNote(_currentlyDisplayedNote!.date);
 | 
						|
    if (nextNote != null) {
 | 
						|
      setState(() {
 | 
						|
        _currentlyDisplayedNote = nextNote;
 | 
						|
        _previousEntryController.text = nextNote.content;
 | 
						|
      });
 | 
						|
      await _checkNavigation();
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  void _loadData() async {
 | 
						|
    String? intervalMinutesStr = await DB.getSetting('popupIntervalMinutes');
 | 
						|
    String? soundFileStr = await DB.getSetting('notificationSound');
 | 
						|
 | 
						|
    int intervalMinutes =
 | 
						|
        int.tryParse(intervalMinutesStr ?? '') ??
 | 
						|
        _defaultPopupInterval.inMinutes;
 | 
						|
    _currentPopupInterval = Duration(minutes: intervalMinutes);
 | 
						|
    _currentNotificationSound = soundFileStr ?? _defaultNotificationSound;
 | 
						|
 | 
						|
    _intervalController.text = intervalMinutes.toString();
 | 
						|
    _soundController.text = _currentNotificationSound;
 | 
						|
 | 
						|
    _startPopupTimer();
 | 
						|
 | 
						|
    final note = await getLatestNote();
 | 
						|
    previousNote = note;
 | 
						|
    _currentlyDisplayedNote = note;
 | 
						|
    _previousEntryController.text = _currentlyDisplayedNote?.content ?? "";
 | 
						|
 | 
						|
    final scratch = await getLatestScratch();
 | 
						|
    _scratchController.text = scratch?.content ?? "";
 | 
						|
    _currentEntryController.text = "";
 | 
						|
 | 
						|
    await _checkNavigation();
 | 
						|
 | 
						|
    debugPrint("Data loaded.");
 | 
						|
  }
 | 
						|
 | 
						|
  // Load volume setting from database
 | 
						|
  Future<void> _loadVolume() async {
 | 
						|
    String? volumeStr = await DB.getSetting('notificationVolume');
 | 
						|
    if (volumeStr != null) {
 | 
						|
      setState(() {
 | 
						|
        _volume = double.tryParse(volumeStr) ?? 0.7;
 | 
						|
        _audioPlayer.setVolume(_linearToLogVolume(_volume));
 | 
						|
      });
 | 
						|
    } else {
 | 
						|
      _audioPlayer.setVolume(_linearToLogVolume(_volume));
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  // Save volume setting to database
 | 
						|
  Future<void> _saveVolume() async {
 | 
						|
    await DB.setSetting('notificationVolume', _volume.toString());
 | 
						|
    debugPrint("Volume saved: $_volume");
 | 
						|
  }
 | 
						|
 | 
						|
  Future<void> _saveData() async {
 | 
						|
    String previousEntry = _previousEntryController.text;
 | 
						|
    String currentEntry = _currentEntryController.text;
 | 
						|
    String scratchContent = _scratchController.text;
 | 
						|
    String intervalStr = _intervalController.text;
 | 
						|
    String soundStr = _soundController.text;
 | 
						|
 | 
						|
    // Handle current entry
 | 
						|
    if (currentEntry.isNotEmpty) {
 | 
						|
      await createNote(currentEntry);
 | 
						|
    }
 | 
						|
 | 
						|
    // Handle scratch pad
 | 
						|
    await createScratch(scratchContent);
 | 
						|
 | 
						|
    // Handle previous/currently displayed note
 | 
						|
    if (_currentlyDisplayedNote != null) {
 | 
						|
      _currentlyDisplayedNote!.content = previousEntry;
 | 
						|
      await updateNote(_currentlyDisplayedNote!);
 | 
						|
 | 
						|
      // If the note was deleted (due to being empty), update the UI state
 | 
						|
      if (previousEntry.isEmpty) {
 | 
						|
        // Check if we need to navigate to another note
 | 
						|
        Note? nextNote = await getLatestNote();
 | 
						|
        setState(() {
 | 
						|
          _currentlyDisplayedNote = nextNote;
 | 
						|
          if (nextNote != null) {
 | 
						|
            _previousEntryController.text = nextNote.content;
 | 
						|
          } else {
 | 
						|
            _previousEntryController.text = "";
 | 
						|
          }
 | 
						|
        });
 | 
						|
        await _checkNavigation();
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    int newIntervalMinutes =
 | 
						|
        int.tryParse(intervalStr) ?? _currentPopupInterval.inMinutes;
 | 
						|
    Duration newInterval = Duration(minutes: newIntervalMinutes);
 | 
						|
    if (newInterval != _currentPopupInterval) {
 | 
						|
      _currentPopupInterval = newInterval;
 | 
						|
      DB.setSetting('popupIntervalMinutes', newIntervalMinutes.toString());
 | 
						|
      _startPopupTimer();
 | 
						|
    } else {
 | 
						|
      DB.setSetting('popupIntervalMinutes', newIntervalMinutes.toString());
 | 
						|
    }
 | 
						|
 | 
						|
    if (soundStr != _currentNotificationSound) {
 | 
						|
      _currentNotificationSound = soundStr;
 | 
						|
      DB.setSetting('notificationSound', soundStr);
 | 
						|
    } else {
 | 
						|
      DB.setSetting('notificationSound', soundStr);
 | 
						|
    }
 | 
						|
 | 
						|
    // Also save volume
 | 
						|
    await _saveVolume();
 | 
						|
 | 
						|
    debugPrint(
 | 
						|
      "Saving data... Current Entry: [${currentEntry.length} chars], Scratch: [${scratchContent.length} chars]",
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  Future<void> _setWindowConfig() async {
 | 
						|
    await windowManager.setAspectRatio(16 / 9);
 | 
						|
  }
 | 
						|
 | 
						|
  // Sanitize FTS query
 | 
						|
  String _sanitizeFtsQuery(String query) {
 | 
						|
    // Simple trimming - the DB layer will handle the complex processing
 | 
						|
    return query.trim();
 | 
						|
  }
 | 
						|
 | 
						|
  // Build rich text with highlights for search results
 | 
						|
  List<InlineSpan> _buildHighlightedText(String highlightedText) {
 | 
						|
    List<InlineSpan> spans = [];
 | 
						|
    // The text comes with <b>highlighted parts</b>
 | 
						|
    RegExp exp = RegExp(r'<b>(.*?)</b>');
 | 
						|
 | 
						|
    int lastIndex = 0;
 | 
						|
    for (final match in exp.allMatches(highlightedText)) {
 | 
						|
      // Add text before the highlight
 | 
						|
      if (match.start > lastIndex) {
 | 
						|
        spans.add(
 | 
						|
          TextSpan(
 | 
						|
            text: highlightedText.substring(lastIndex, match.start),
 | 
						|
            style: const TextStyle(
 | 
						|
              fontSize: 13,
 | 
						|
            ), // Smaller font for regular text
 | 
						|
          ),
 | 
						|
        );
 | 
						|
      }
 | 
						|
 | 
						|
      // Add the highlighted text
 | 
						|
      spans.add(
 | 
						|
        TextSpan(
 | 
						|
          text: match.group(1),
 | 
						|
          style: const TextStyle(
 | 
						|
            fontWeight: FontWeight.bold,
 | 
						|
            backgroundColor: Colors.yellow,
 | 
						|
            color: Colors.black,
 | 
						|
            fontSize: 13, // Smaller font for highlighted text
 | 
						|
          ),
 | 
						|
        ),
 | 
						|
      );
 | 
						|
 | 
						|
      lastIndex = match.end;
 | 
						|
    }
 | 
						|
 | 
						|
    // Add any remaining text
 | 
						|
    if (lastIndex < highlightedText.length) {
 | 
						|
      spans.add(
 | 
						|
        TextSpan(
 | 
						|
          text: highlightedText.substring(lastIndex),
 | 
						|
          style: const TextStyle(fontSize: 13), // Smaller font for regular text
 | 
						|
        ),
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    return spans;
 | 
						|
  }
 | 
						|
 | 
						|
  // Show search dialog
 | 
						|
  void _showSearchDialog() {
 | 
						|
    _searchController.clear();
 | 
						|
    _searchResults = [];
 | 
						|
    _isSearching = false;
 | 
						|
 | 
						|
    showDialog(
 | 
						|
      context: context,
 | 
						|
      builder: (BuildContext context) {
 | 
						|
        return StatefulBuilder(
 | 
						|
          builder: (context, dialogSetState) {
 | 
						|
            return AlertDialog(
 | 
						|
              title: const Text('Search Notes'),
 | 
						|
              content: SizedBox(
 | 
						|
                width: MediaQuery.of(context).size.width * 0.7,
 | 
						|
                height: MediaQuery.of(context).size.height * 0.7,
 | 
						|
                child: Column(
 | 
						|
                  children: [
 | 
						|
                    TextField(
 | 
						|
                      controller: _searchController,
 | 
						|
                      decoration: const InputDecoration(
 | 
						|
                        labelText: 'Search Query',
 | 
						|
                        hintText: 'e.g. wifi or meeting',
 | 
						|
                        border: OutlineInputBorder(),
 | 
						|
                        prefixIcon: Icon(Icons.search),
 | 
						|
                      ),
 | 
						|
                      autofocus: true,
 | 
						|
                      onChanged: (value) async {
 | 
						|
                        // Start search and update dialog state
 | 
						|
                        if (value.isEmpty) {
 | 
						|
                          dialogSetState(() {
 | 
						|
                            _searchResults = [];
 | 
						|
                            _isSearching = false;
 | 
						|
                          });
 | 
						|
                          return;
 | 
						|
                        }
 | 
						|
 | 
						|
                        dialogSetState(() {
 | 
						|
                          _isSearching = true;
 | 
						|
                        });
 | 
						|
 | 
						|
                        // Escape special characters to prevent SQLite FTS syntax errors
 | 
						|
                        String trimmedQuery = _sanitizeFtsQuery(value);
 | 
						|
 | 
						|
                        // Debounce search
 | 
						|
                        _searchDebounceTimer?.cancel();
 | 
						|
                        _searchDebounceTimer = Timer(
 | 
						|
                          const Duration(milliseconds: 300),
 | 
						|
                          () async {
 | 
						|
                            try {
 | 
						|
                              final results = await searchNotes(trimmedQuery);
 | 
						|
 | 
						|
                              // Filter out empty notes (which may exist in the search index but were deleted)
 | 
						|
                              final filteredResults =
 | 
						|
                                  results
 | 
						|
                                      .where((note) => note.content.isNotEmpty)
 | 
						|
                                      .toList();
 | 
						|
 | 
						|
                              // Important: update the dialog state after search completes
 | 
						|
                              dialogSetState(() {
 | 
						|
                                _searchResults = filteredResults;
 | 
						|
                                _isSearching = false;
 | 
						|
                              });
 | 
						|
                            } catch (e) {
 | 
						|
                              debugPrint('Search error: $e');
 | 
						|
                              dialogSetState(() {
 | 
						|
                                _searchResults = [];
 | 
						|
                                _isSearching = false;
 | 
						|
                              });
 | 
						|
                            }
 | 
						|
                          },
 | 
						|
                        );
 | 
						|
                      },
 | 
						|
                    ),
 | 
						|
                    const SizedBox(height: 16),
 | 
						|
                    _isSearching
 | 
						|
                        ? const Center(child: CircularProgressIndicator())
 | 
						|
                        : Expanded(
 | 
						|
                          child:
 | 
						|
                              _searchResults.isEmpty
 | 
						|
                                  ? const Center(
 | 
						|
                                    child: Text(
 | 
						|
                                      'No results. Try a different search term.',
 | 
						|
                                    ),
 | 
						|
                                  )
 | 
						|
                                  : ListView.builder(
 | 
						|
                                    itemCount: _searchResults.length,
 | 
						|
                                    itemBuilder: (context, index) {
 | 
						|
                                      final note = _searchResults[index];
 | 
						|
                                      return Card(
 | 
						|
                                        margin: const EdgeInsets.only(
 | 
						|
                                          bottom:
 | 
						|
                                              6, // Reduced margin between cards
 | 
						|
                                        ),
 | 
						|
                                        child: ListTile(
 | 
						|
                                          dense:
 | 
						|
                                              true, // Makes the ListTile more compact
 | 
						|
                                          contentPadding:
 | 
						|
                                              const EdgeInsets.symmetric(
 | 
						|
                                                horizontal: 12,
 | 
						|
                                                vertical: 2,
 | 
						|
                                              ), // Tighter padding
 | 
						|
                                          title: Text(
 | 
						|
                                            note.displayDate,
 | 
						|
                                            style: const TextStyle(
 | 
						|
                                              fontWeight: FontWeight.bold,
 | 
						|
                                              fontSize:
 | 
						|
                                                  12, // Smaller font for date
 | 
						|
                                            ),
 | 
						|
                                          ),
 | 
						|
                                          subtitle:
 | 
						|
                                              note.snippet != null
 | 
						|
                                                  ? Text.rich(
 | 
						|
                                                    TextSpan(
 | 
						|
                                                      children:
 | 
						|
                                                          _buildHighlightedText(
 | 
						|
                                                            note.snippet!,
 | 
						|
                                                          ),
 | 
						|
                                                    ),
 | 
						|
                                                  )
 | 
						|
                                                  : Text(
 | 
						|
                                                    note.content.length > 200
 | 
						|
                                                        ? '${note.content.substring(0, 200)}...'
 | 
						|
                                                        : note.content,
 | 
						|
                                                    style: const TextStyle(
 | 
						|
                                                      fontSize: 13,
 | 
						|
                                                    ), // Smaller font for content
 | 
						|
                                                  ),
 | 
						|
                                          isThreeLine: true,
 | 
						|
                                          onTap: () async {
 | 
						|
                                            // Save current note if needed
 | 
						|
                                            if (_currentlyDisplayedNote !=
 | 
						|
                                                null) {
 | 
						|
                                              _currentlyDisplayedNote!.content =
 | 
						|
                                                  _previousEntryController.text;
 | 
						|
                                              await updateNote(
 | 
						|
                                                _currentlyDisplayedNote!,
 | 
						|
                                              );
 | 
						|
                                            }
 | 
						|
 | 
						|
                                            // Navigate to the selected note
 | 
						|
                                            Navigator.of(context).pop();
 | 
						|
                                            setState(() {
 | 
						|
                                              _currentlyDisplayedNote = note;
 | 
						|
                                              _previousEntryController.text =
 | 
						|
                                                  note.content;
 | 
						|
                                            });
 | 
						|
                                            _checkNavigation();
 | 
						|
                                          },
 | 
						|
                                        ),
 | 
						|
                                      );
 | 
						|
                                    },
 | 
						|
                                  ),
 | 
						|
                        ),
 | 
						|
                  ],
 | 
						|
                ),
 | 
						|
              ),
 | 
						|
              actions: [
 | 
						|
                TextButton(
 | 
						|
                  onPressed: () {
 | 
						|
                    Navigator.of(context).pop();
 | 
						|
                  },
 | 
						|
                  child: const Text('Close'),
 | 
						|
                ),
 | 
						|
              ],
 | 
						|
            );
 | 
						|
          },
 | 
						|
        );
 | 
						|
      },
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  // Volume slider widget that uses a logarithmic scale
 | 
						|
  Widget _buildVolumeSlider() {
 | 
						|
    return Row(
 | 
						|
      mainAxisSize: MainAxisSize.min,
 | 
						|
      children: [
 | 
						|
        const Icon(Icons.volume_mute, size: 16),
 | 
						|
        SizedBox(
 | 
						|
          width: 100,
 | 
						|
          child: SliderTheme(
 | 
						|
            data: SliderTheme.of(context).copyWith(
 | 
						|
              trackHeight: 4.0,
 | 
						|
              thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8.0),
 | 
						|
              overlayShape: const RoundSliderOverlayShape(overlayRadius: 14.0),
 | 
						|
            ),
 | 
						|
            child: Slider(
 | 
						|
              value: _volume,
 | 
						|
              min: 0.0,
 | 
						|
              max: 1.0,
 | 
						|
              divisions: 20, // More divisions for finer control
 | 
						|
              onChanged: (value) {
 | 
						|
                setState(() {
 | 
						|
                  _volume = value;
 | 
						|
                });
 | 
						|
                _audioPlayer.setVolume(_linearToLogVolume(value));
 | 
						|
              },
 | 
						|
              onChangeEnd: (value) {
 | 
						|
                _saveVolume();
 | 
						|
              },
 | 
						|
            ),
 | 
						|
          ),
 | 
						|
        ),
 | 
						|
        const Icon(Icons.volume_up, size: 16),
 | 
						|
      ],
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    // Wrap Scaffold with RawKeyboardListener as workaround for Escape key
 | 
						|
    return RawKeyboardListener(
 | 
						|
      focusNode:
 | 
						|
          FocusNode()
 | 
						|
            ..requestFocus(), // Request focus to ensure keyboard events are captured
 | 
						|
      onKey: (RawKeyEvent event) {
 | 
						|
        if (event is RawKeyDownEvent) {
 | 
						|
          // Handle Escape to close window
 | 
						|
          if (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();
 | 
						|
          }
 | 
						|
          // Handle Ctrl+F to open search
 | 
						|
          else if (event.logicalKey == LogicalKeyboardKey.keyF &&
 | 
						|
              (event.isControlPressed || event.isMetaPressed)) {
 | 
						|
            debugPrint("Ctrl+F pressed, opening search dialog");
 | 
						|
            _showSearchDialog();
 | 
						|
          }
 | 
						|
        }
 | 
						|
      },
 | 
						|
      child: Scaffold(
 | 
						|
        appBar: AppBar(
 | 
						|
          title: const Text('Journaler'),
 | 
						|
          actions: <Widget>[
 | 
						|
            // Add search button
 | 
						|
            IconButton(
 | 
						|
              icon: const Icon(Icons.search),
 | 
						|
              tooltip: 'Search Notes',
 | 
						|
              onPressed: _showSearchDialog,
 | 
						|
            ),
 | 
						|
            // Group Label and Input for Interval
 | 
						|
            Padding(
 | 
						|
              padding: const EdgeInsets.symmetric(
 | 
						|
                vertical: 8.0,
 | 
						|
              ).copyWith(left: 8.0), // Add padding
 | 
						|
              child: Row(
 | 
						|
                mainAxisSize: MainAxisSize.min, // Use minimum space
 | 
						|
                children: [
 | 
						|
                  const Text("Interval (m):"),
 | 
						|
                  const SizedBox(width: 4), // Space between label and input
 | 
						|
                  SizedBox(
 | 
						|
                    width: 60, // Constrain width
 | 
						|
                    child: TextField(
 | 
						|
                      controller: _intervalController,
 | 
						|
                      // textAlignVertical: TextAlignVertical.center, // Let default alignment handle it
 | 
						|
                      decoration: const InputDecoration(
 | 
						|
                        border: OutlineInputBorder(),
 | 
						|
                        isDense: true,
 | 
						|
                        contentPadding: EdgeInsets.symmetric(
 | 
						|
                          horizontal: 8.0,
 | 
						|
                          vertical: 8.0,
 | 
						|
                        ),
 | 
						|
                      ),
 | 
						|
                      keyboardType: TextInputType.number,
 | 
						|
                      inputFormatters: <TextInputFormatter>[
 | 
						|
                        FilteringTextInputFormatter.digitsOnly,
 | 
						|
                      ],
 | 
						|
                    ),
 | 
						|
                  ),
 | 
						|
                ],
 | 
						|
              ),
 | 
						|
            ),
 | 
						|
            // Group Label and Input for Sound
 | 
						|
            Padding(
 | 
						|
              padding: const EdgeInsets.symmetric(
 | 
						|
                vertical: 8.0,
 | 
						|
                horizontal: 8.0,
 | 
						|
              ),
 | 
						|
              child: Row(
 | 
						|
                mainAxisSize: MainAxisSize.min,
 | 
						|
                children: [
 | 
						|
                  const Text("Sound:"),
 | 
						|
                  const SizedBox(width: 4),
 | 
						|
                  SizedBox(
 | 
						|
                    width: 150, // Constrain width
 | 
						|
                    child: TextField(
 | 
						|
                      controller: _soundController,
 | 
						|
                      // textAlignVertical: TextAlignVertical.center,
 | 
						|
                      decoration: const InputDecoration(
 | 
						|
                        border: OutlineInputBorder(),
 | 
						|
                        isDense: true,
 | 
						|
                        contentPadding: EdgeInsets.symmetric(
 | 
						|
                          horizontal: 8.0,
 | 
						|
                          vertical: 8.0,
 | 
						|
                        ),
 | 
						|
                        hintText: 'sound.mp3',
 | 
						|
                      ),
 | 
						|
                    ),
 | 
						|
                  ),
 | 
						|
                ],
 | 
						|
              ),
 | 
						|
            ),
 | 
						|
            // Volume Control Slider
 | 
						|
            Padding(
 | 
						|
              padding: const EdgeInsets.symmetric(horizontal: 8.0),
 | 
						|
              child: _buildVolumeSlider(),
 | 
						|
            ),
 | 
						|
            // Test Sound Button
 | 
						|
            Padding(
 | 
						|
              padding: const EdgeInsets.symmetric(vertical: 8.0),
 | 
						|
              child: IconButton(
 | 
						|
                icon: const Icon(Icons.volume_up),
 | 
						|
                tooltip: 'Test Sound',
 | 
						|
                onPressed: _playSound,
 | 
						|
              ),
 | 
						|
            ),
 | 
						|
            const SizedBox(width: 10),
 | 
						|
          ],
 | 
						|
        ),
 | 
						|
        body: Padding(
 | 
						|
          padding: const EdgeInsets.all(8.0),
 | 
						|
          child: Row(
 | 
						|
            crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
						|
            children: [
 | 
						|
              Expanded(
 | 
						|
                flex: 9,
 | 
						|
                child: Listener(
 | 
						|
                  behavior: HitTestBehavior.opaque,
 | 
						|
                  onPointerSignal: (pointerSignal) {
 | 
						|
                    if (pointerSignal is PointerScrollEvent) {
 | 
						|
                      if (pointerSignal.scrollDelta.dy < 0) {
 | 
						|
                        if (_canGoPrevious) {
 | 
						|
                          _goToPreviousNote();
 | 
						|
                        }
 | 
						|
                      } else if (pointerSignal.scrollDelta.dy > 0) {
 | 
						|
                        if (_canGoNext) {
 | 
						|
                          _goToNextNote();
 | 
						|
                        }
 | 
						|
                      }
 | 
						|
                    }
 | 
						|
                  },
 | 
						|
                  child: Column(
 | 
						|
                    crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
						|
                    children: [
 | 
						|
                      // Combine Label, Buttons, and TextField for Previous Entry
 | 
						|
                      Row(
 | 
						|
                        children: [
 | 
						|
                          Expanded(
 | 
						|
                            child: Text(
 | 
						|
                              _currentlyDisplayedNote?.displayDate ==
 | 
						|
                                      previousNote?.displayDate
 | 
						|
                                  ? 'Previous Entry: ${_currentlyDisplayedNote?.displayDate ?? 'N/A'}'
 | 
						|
                                  : 'Entry: ${_currentlyDisplayedNote?.displayDate ?? 'N/A'}',
 | 
						|
                              style: TextStyle(
 | 
						|
                                fontSize: 18,
 | 
						|
                                color: Colors.grey,
 | 
						|
                              ),
 | 
						|
                            ),
 | 
						|
                          ),
 | 
						|
                          IconButton(
 | 
						|
                            icon: const Icon(Icons.arrow_back),
 | 
						|
                            tooltip: 'Previous Note',
 | 
						|
                            onPressed:
 | 
						|
                                _canGoPrevious ? _goToPreviousNote : null,
 | 
						|
                          ),
 | 
						|
                          IconButton(
 | 
						|
                            icon: const Icon(Icons.arrow_forward),
 | 
						|
                            tooltip: 'Next Note',
 | 
						|
                            onPressed: _canGoNext ? _goToNextNote : null,
 | 
						|
                          ),
 | 
						|
                        ],
 | 
						|
                      ),
 | 
						|
                      Expanded(
 | 
						|
                        child: TextField(
 | 
						|
                          controller: _previousEntryController,
 | 
						|
                          readOnly: false, // Always allow editing
 | 
						|
                          maxLines: null,
 | 
						|
                          expands: true,
 | 
						|
                          style: Theme.of(context).textTheme.bodyMedium,
 | 
						|
                          decoration: InputDecoration(
 | 
						|
                            hintText:
 | 
						|
                                _currentlyDisplayedNote?.displayDate !=
 | 
						|
                                        previousNote?.displayDate
 | 
						|
                                    ? 'Viewing note from ${_currentlyDisplayedNote?.displayDate} (Editable)'
 | 
						|
                                    : 'Latest Note: ${_currentlyDisplayedNote?.displayDate ?? 'N/A'}',
 | 
						|
                            border: const OutlineInputBorder(),
 | 
						|
                            filled:
 | 
						|
                                _currentlyDisplayedNote?.displayDate !=
 | 
						|
                                        previousNote?.displayDate,
 | 
						|
                            fillColor:
 | 
						|
                                _currentlyDisplayedNote?.displayDate !=
 | 
						|
                                        previousNote?.displayDate
 | 
						|
                                    ? Colors.grey.withOpacity(0.1)
 | 
						|
                                    : null,
 | 
						|
                          ),
 | 
						|
                        ),
 | 
						|
                      ),
 | 
						|
                      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?)',
 | 
						|
                          ),
 | 
						|
                        ),
 | 
						|
                      ),
 | 
						|
                    ],
 | 
						|
                  ),
 | 
						|
                ),
 | 
						|
              ),
 | 
						|
              const SizedBox(width: 8),
 | 
						|
              Expanded(
 | 
						|
                flex: 4, // Adjust flex factor as needed
 | 
						|
                child: TextField(
 | 
						|
                  controller: _scratchController,
 | 
						|
                  maxLines: null,
 | 
						|
                  expands: true,
 | 
						|
                  style:
 | 
						|
                      Theme.of(
 | 
						|
                        context,
 | 
						|
                      ).textTheme.bodyMedium, // Apply theme text style
 | 
						|
                  decoration: const InputDecoration(labelText: 'Scratch'),
 | 
						|
                ),
 | 
						|
              ),
 | 
						|
            ],
 | 
						|
          ),
 | 
						|
        ),
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
// --- End Actions and Shortcuts ---
 |