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
545 lines
18 KiB
Dart
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 ---
|