Files
journaler/lib/main.dart
PhatPhuckDave 5ea62a1216 Do focus on popup
Because the window APPEARS over our current window
But isn't focuseded
So we canj't see our fucking window
And we can't close the new fucking window
So it's the worst of both worlds
FUCK
2025-04-23 11:23:11 +02:00

545 lines
18 KiB
Dart

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';
import 'package:flutter/gestures.dart';
// TODO: Sound does not play when ran from a different workdir? Weird
// Default values - will be replaced by DB values if they exist
const Duration _defaultPopupInterval = Duration(minutes: 20);
const String _defaultNotificationSound = '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<MainPage> createState() => MainPageState();
}
class MainPageState extends State<MainPage> 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 _scratchController = TextEditingController();
final TextEditingController _intervalController = TextEditingController();
final TextEditingController _soundController = TextEditingController();
Note? previousNote;
Note? _currentlyDisplayedNote;
Duration _currentPopupInterval = _defaultPopupInterval;
String _currentNotificationSound = _defaultNotificationSound;
bool _canGoPrevious = false;
bool _canGoNext = false;
Timer? _popupTimer;
Timer? _debounceTimer;
@override
void initState() {
super.initState();
windowManager.addListener(this);
_initSystemTray();
_loadData();
windowManager.setPreventClose(true);
_setWindowConfig();
}
@override
void dispose() {
windowManager.removeListener(this);
_popupTimer?.cancel();
_debounceTimer?.cancel();
_previousEntryController.dispose();
_currentEntryController.dispose();
_currentEntryFocusNode.dispose();
_scratchController.dispose();
_intervalController.dispose();
_soundController.dispose();
_audioPlayer.dispose();
super.dispose();
}
@override
void onWindowClose() {
_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();
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<void> _playSound() async {
await _audioPlayer.stop();
try {
await _audioPlayer.play(AssetSource('sounds/$_currentNotificationSound'));
debugPrint("Played sound: $_currentNotificationSound");
} 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;
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;
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.");
}
void _saveData() async {
String previousEntry = _previousEntryController.text;
String currentEntry = _currentEntryController.text;
String scratchContent = _scratchController.text;
String intervalStr = _intervalController.text;
String soundStr = _soundController.text;
await createNote(currentEntry);
await createScratch(scratchContent);
if (previousNote != null) {
previousNote!.content = previousEntry;
await updateNote(previousNote!);
}
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);
}
debugPrint(
"Saving data... Current Entry: [${currentEntry.length} chars], Scratch: [${scratchContent.length} chars]",
);
}
Future<void> _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: <Widget>[
// 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',
),
),
),
],
),
),
// 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?.date == previousNote?.date
? 'Previous Entry (Latest)'
: 'Entry: ${_currentlyDisplayedNote?.date ?? '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: _currentlyDisplayedNote?.date != previousNote?.date,
maxLines: null,
expands: true,
style: Theme.of(context).textTheme.bodyMedium,
decoration: InputDecoration(
hintText: _currentlyDisplayedNote?.date != previousNote?.date
? 'Viewing note from ${_currentlyDisplayedNote?.date} (Read-Only)'
: 'Latest Note',
border: const OutlineInputBorder(),
filled: _currentlyDisplayedNote?.date != previousNote?.date,
fillColor: _currentlyDisplayedNote?.date != previousNote?.date
? 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 ---