413 lines
15 KiB
Dart
413 lines
15 KiB
Dart
import 'dart:async';
|
|
import 'dart:io'; // Required for Platform check
|
|
import 'package:flutter/material.dart';
|
|
import 'package:system_tray/system_tray.dart';
|
|
import 'package:window_manager/window_manager.dart';
|
|
import 'package:audioplayers/audioplayers.dart';
|
|
// For path joining
|
|
|
|
// --- Configuration ---
|
|
const Duration popupInterval = Duration(hours: 1); // How often to pop up
|
|
const String notificationSound =
|
|
'notification.wav'; // Sound file in assets/sounds/
|
|
// --------------------
|
|
|
|
void main() async {
|
|
WidgetsFlutterBinding.ensureInitialized();
|
|
// Must add this line.
|
|
await windowManager.ensureInitialized();
|
|
|
|
// Use it only after calling `hiddenWindowAtLaunch`
|
|
WindowOptions windowOptions = const WindowOptions(
|
|
size: Size(1600, 900), // Increased size to 1600x900
|
|
center: true,
|
|
backgroundColor: Colors.transparent,
|
|
skipTaskbar: false,
|
|
titleBarStyle: TitleBarStyle.normal,
|
|
);
|
|
|
|
// Hide window at launch
|
|
windowManager.waitUntilReadyToShow(windowOptions, () async {
|
|
await windowManager.hide(); // Start hidden
|
|
});
|
|
|
|
runApp(const JournalerApp());
|
|
}
|
|
|
|
class JournalerApp extends StatelessWidget {
|
|
const JournalerApp({super.key});
|
|
|
|
// Define the light theme
|
|
static final ThemeData lightTheme = ThemeData(
|
|
brightness: Brightness.light,
|
|
primarySwatch: Colors.blue, // Or use ColorScheme.fromSeed(seedColor: Colors.blue)
|
|
visualDensity: VisualDensity.adaptivePlatformDensity,
|
|
scaffoldBackgroundColor: Colors.grey[100], // Slightly off-white
|
|
appBarTheme: const AppBarTheme(
|
|
backgroundColor: Colors.blue,
|
|
foregroundColor: Colors.white, // Title and icons
|
|
),
|
|
inputDecorationTheme: InputDecorationTheme(
|
|
border: const OutlineInputBorder(),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderSide: BorderSide(color: Colors.blue.shade600, width: 2.0),
|
|
),
|
|
labelStyle: TextStyle(color: Colors.grey[700]),
|
|
),
|
|
// Add other theme properties like textTheme if needed
|
|
);
|
|
|
|
// Define the dark theme
|
|
static final ThemeData darkTheme = ThemeData(
|
|
brightness: Brightness.dark,
|
|
primarySwatch: Colors.blue, // Keep blue, or choose a different shade
|
|
// Or use ColorScheme.fromSeed(seedColor: Colors.blue, brightness: Brightness.dark)
|
|
visualDensity: VisualDensity.adaptivePlatformDensity,
|
|
scaffoldBackgroundColor: Colors.grey[900], // Dark background
|
|
appBarTheme: AppBarTheme(
|
|
backgroundColor: Colors.grey[850], // Darker app bar
|
|
foregroundColor: Colors.white, // Title and icons
|
|
),
|
|
inputDecorationTheme: InputDecorationTheme(
|
|
border: const OutlineInputBorder(
|
|
borderSide: BorderSide(color: Colors.grey), // Lighter border for dark theme
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderSide: BorderSide(color: Colors.blue.shade300, width: 2.0),
|
|
),
|
|
labelStyle: TextStyle(color: Colors.grey[400]), // Lighter label for dark theme
|
|
// Ensure content padding is applied if needed, or use default
|
|
// contentPadding: const EdgeInsets.all(8.0), // This was in TextField, maybe move to theme?
|
|
),
|
|
// Ensure text field text color is readable
|
|
textTheme: const TextTheme(
|
|
bodyMedium: TextStyle(color: Colors.white), // Default text style
|
|
// Define other text styles as needed
|
|
),
|
|
// Define icon theme for app bar if needed, though foregroundColor often handles it
|
|
// iconTheme: const IconThemeData(color: Colors.white),
|
|
);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MaterialApp(
|
|
title: 'Journaler',
|
|
theme: lightTheme, // Use the light theme definition
|
|
darkTheme: darkTheme, // Use the dark theme definition
|
|
themeMode: ThemeMode.system, // Automatically switch based on system settings
|
|
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 AppWindow _appWindow = AppWindow(); // REMOVED - Not used
|
|
final SystemTray _systemTray = SystemTray();
|
|
final AudioPlayer _audioPlayer = AudioPlayer();
|
|
|
|
final TextEditingController _previousEntryController =
|
|
TextEditingController();
|
|
final TextEditingController _currentEntryController = TextEditingController();
|
|
final TextEditingController _todoController = TextEditingController();
|
|
|
|
Timer? _popupTimer;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
windowManager.addListener(this);
|
|
_initSystemTray();
|
|
_loadData(); // Placeholder for loading initial data
|
|
_startPopupTimer();
|
|
// Prevent window from closing, instead hide it
|
|
// Needs `implements WindowListener` and `windowManager.addListener(this);`
|
|
// and `windowManager.removeListener(this);` in dispose
|
|
windowManager.setPreventClose(true);
|
|
// Set aspect ratio after ensuring initialized
|
|
_setWindowConfig();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
windowManager.removeListener(this);
|
|
_popupTimer?.cancel();
|
|
_previousEntryController.dispose();
|
|
_currentEntryController.dispose();
|
|
_todoController.dispose();
|
|
_audioPlayer.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
// --- Window Listener --- //
|
|
@override
|
|
void onWindowClose() {
|
|
_saveData(); // Placeholder for saving data
|
|
windowManager.hide(); // Hide instead of closing
|
|
// Alternatively, to fully close:
|
|
// windowManager.destroy();
|
|
}
|
|
|
|
@override
|
|
void onWindowFocus() {
|
|
// Maybe reload data if needed when window becomes active
|
|
setState(() {
|
|
// If you need to refresh state
|
|
});
|
|
}
|
|
|
|
// --- System Tray --- //
|
|
Future<void> _initSystemTray() async {
|
|
// Use simple relative paths for assets declared in pubspec.yaml
|
|
// The build process handles packaging these assets correctly.
|
|
String iconPath =
|
|
Platform.isWindows ? 'assets/app_icon.ico' : 'assets/app_icon.png';
|
|
|
|
// IMPORTANT: You need to create an icon file named app_icon.ico (for Windows)
|
|
// and/or app_icon.png (for Mac/Linux) and place it in the 'assets/' directory.
|
|
// Create the 'assets' directory if it doesn't exist at the root of your project.
|
|
// Then, ensure your pubspec.yaml lists the 'assets/' directory:
|
|
// flutter:
|
|
// uses-material-design: true
|
|
// assets:
|
|
// - assets/
|
|
// - assets/sounds/ # Keep this if you have it
|
|
|
|
// Check if the asset file exists before trying to use it (optional but recommended)
|
|
// Note: This basic check works during development. Accessing assets might
|
|
// require different handling in production builds depending on the platform.
|
|
// try {
|
|
// await rootBundle.load(iconPath); // Check if asset is loadable
|
|
// } catch (_) {
|
|
// debugPrint("Error: System tray icon '$iconPath' not found in assets.");
|
|
// // Handle the error, maybe use a default icon or skip tray init
|
|
// return;
|
|
// }
|
|
|
|
await _systemTray.initSystemTray(
|
|
// title: "Journaler", // Optional: Title shown on hover (might not work on all OS)
|
|
iconPath: iconPath, // Use the corrected path
|
|
toolTip: "Journaler",
|
|
);
|
|
|
|
// Handle system tray menu item clicks
|
|
_systemTray.registerSystemTrayEventHandler((eventName) {
|
|
debugPrint("System Tray Event: $eventName");
|
|
if (eventName == kSystemTrayEventClick) {
|
|
_showWindow();
|
|
} else if (eventName == kSystemTrayEventRightClick) {
|
|
// Optional: Show a context menu on right click
|
|
// _systemTray.popUpContextMenu() - need to define menu items first
|
|
}
|
|
});
|
|
}
|
|
|
|
// --- Periodic Popup & Sound --- //
|
|
void _startPopupTimer() {
|
|
_popupTimer = Timer.periodic(popupInterval, (timer) {
|
|
_showWindowAndPlaySound();
|
|
});
|
|
}
|
|
|
|
Future<void> _showWindowAndPlaySound() async {
|
|
await _showWindow();
|
|
await _playSound();
|
|
}
|
|
|
|
Future<void> _showWindow() async {
|
|
bool isVisible = await windowManager.isVisible();
|
|
if (!isVisible) {
|
|
// Ensure size and position before showing
|
|
await windowManager.setSize(const Size(1600, 900)); // Set desired size
|
|
await windowManager.center(); // Center the window
|
|
await windowManager.show();
|
|
await windowManager.focus();
|
|
} else {
|
|
// If already visible, just bring to front and focus
|
|
await windowManager.focus();
|
|
}
|
|
}
|
|
|
|
Future<void> _playSound() async {
|
|
try {
|
|
// Assumes the sound file is in assets/sounds/
|
|
await _audioPlayer.play(AssetSource('sounds/$notificationSound'));
|
|
debugPrint("Played sound: $notificationSound");
|
|
} catch (e) {
|
|
debugPrint("Error playing sound: $e");
|
|
// Handle error, e.g., show a notification or log
|
|
}
|
|
}
|
|
|
|
// --- Data Handling (Placeholders) --- //
|
|
void _loadData() {
|
|
// TODO: Implement logic to load previous entry and todo list
|
|
// Example:
|
|
// _previousEntryController.text = await loadPreviousEntryFromDatabase();
|
|
// _todoController.text = await loadTodoListFromFile();
|
|
_previousEntryController.text =
|
|
"This is a placeholder for the previous entry.";
|
|
_todoController.text = "- Placeholder Todo 1\n- Placeholder Todo 2";
|
|
_currentEntryController.text = ""; // Current entry always starts empty
|
|
debugPrint("Data loaded (placeholder).");
|
|
}
|
|
|
|
void _saveData() {
|
|
// TODO: Implement logic to save the current entry and todo list
|
|
// This is called when the window is closed (hidden) or the save button is pressed.
|
|
String currentEntry = _currentEntryController.text;
|
|
String todoList = _todoController.text;
|
|
|
|
print(
|
|
"Saving data (placeholder)... Current Entry: [${currentEntry.length} chars], Todo: [${todoList.length} chars]",
|
|
);
|
|
|
|
// --- Your persistence logic goes here ---
|
|
// Example:
|
|
// await saveEntryToDatabase(currentEntry);
|
|
// await saveTodoListToFile(todoList);
|
|
// ---------------------------------------
|
|
|
|
// You might want to update the previous entry for the *next* session here,
|
|
// or handle this logic when loading data next time.
|
|
// Example (simplistic, assumes immediate update for next view):
|
|
// setState(() {
|
|
// _previousEntryController.text = currentEntry;
|
|
// _currentEntryController.clear(); // Clear current entry after saving
|
|
// });
|
|
|
|
// Potentially clear current entry after saving, or handle it on next load
|
|
// _currentEntryController.clear(); // Decide if you want to clear it immediately
|
|
}
|
|
|
|
// Helper to set initial window config like aspect ratio
|
|
Future<void> _setWindowConfig() async {
|
|
// Wait a moment to ensure window manager is fully ready after init
|
|
// Might not be strictly necessary but can prevent race conditions
|
|
// await Future.delayed(const Duration(milliseconds: 100));
|
|
await windowManager.setAspectRatio(16 / 9);
|
|
// Optionally set min/max size if desired
|
|
// await windowManager.setMinimumSize(const Size(800, 450));
|
|
}
|
|
|
|
// --- UI Build --- //
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('Journaler'),
|
|
// Optional: Add a save button or other actions
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(Icons.save),
|
|
onPressed: _saveData,
|
|
tooltip: 'Save Current Entry & Todo',
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.volume_up),
|
|
onPressed: _playSound, // For testing sound
|
|
tooltip: 'Test Sound',
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.timer),
|
|
onPressed: _showWindowAndPlaySound, // For testing popup
|
|
tooltip: 'Test Popup',
|
|
),
|
|
],
|
|
),
|
|
body: Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
// Main journal area (8-10 columns)
|
|
Expanded(
|
|
flex:
|
|
9, // Adjust flex factor (e.g., 8, 9, 10) for desired width ratio
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
// Previous Entry (read-only conceptually, but TextField for easy display)
|
|
Expanded(
|
|
child: TextField(
|
|
controller: _previousEntryController,
|
|
maxLines: null, // Allows unlimited lines
|
|
expands: true, // Fills the available space
|
|
readOnly: true, // Make it non-editable
|
|
decoration: const InputDecoration(
|
|
labelText: 'Previous Entry',
|
|
// border: OutlineInputBorder(), // Handled by theme
|
|
// contentPadding: EdgeInsets.all(8.0), // Handled by theme or default
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 8), // Spacing
|
|
// Current Entry
|
|
Expanded(
|
|
child: TextField(
|
|
controller: _currentEntryController,
|
|
maxLines: null,
|
|
expands: true,
|
|
autofocus: true, // Focus here when window appears
|
|
decoration: const InputDecoration(
|
|
labelText: 'Current Entry (What\'s on your mind?)',
|
|
// border: OutlineInputBorder(), // Handled by theme
|
|
// contentPadding: EdgeInsets.all(8.0), // Handled by theme or default
|
|
),
|
|
onChanged: (text) {
|
|
// Optional: Add auto-save logic here if desired
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 8), // Spacing between columns
|
|
// Todo List area (remaining columns)
|
|
Expanded(
|
|
flex: 3, // Adjust flex factor (12 - 9 = 3, or 12 - 8 = 4, etc.)
|
|
child: TextField(
|
|
controller: _todoController,
|
|
maxLines: null,
|
|
expands: true,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Todo List',
|
|
// border: OutlineInputBorder(), // Handled by theme
|
|
// contentPadding: EdgeInsets.all(8.0), // Handled by theme or default
|
|
),
|
|
onChanged: (text) {
|
|
// Optional: Auto-save Todo list changes
|
|
// Consider debouncing if saving frequently
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// REMOVED - Helper class for AppWindow interactions (optional, but good practice)
|
|
// class AppWindow {
|
|
// Future<void> init() async {
|
|
// // Can add more window setup here if needed
|
|
// }
|
|
//
|
|
// Future<void> show() async {
|
|
// await windowManager.show();
|
|
// await windowManager.focus();
|
|
// }
|
|
//
|
|
// Future<void> hide() async {
|
|
// await windowManager.hide();
|
|
// }
|
|
// }
|