2 Commits

Author SHA1 Message Date
9d39cb09df Squash merge feature/fts into master 2025-04-23 14:51:24 +02:00
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
3 changed files with 498 additions and 43 deletions

View File

@@ -33,6 +33,29 @@ CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY NOT NULL, key TEXT PRIMARY KEY NOT NULL,
value TEXT NOT NULL value TEXT NOT NULL
); );
-- Create virtual FTS5 table for searching notes content
CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(
content,
date,
content='notes',
content_rowid='id'
);
-- Trigger to keep FTS table in sync with notes table when inserting
CREATE TRIGGER IF NOT EXISTS notes_ai AFTER INSERT ON notes BEGIN
INSERT INTO notes_fts(rowid, content, date) VALUES (new.id, new.content, new.date);
END;
-- Trigger to keep FTS table in sync when deleting notes
CREATE TRIGGER IF NOT EXISTS notes_ad AFTER DELETE ON notes BEGIN
DELETE FROM notes_fts WHERE rowid = old.id;
END;
-- Trigger to keep FTS table in sync when updating notes
CREATE TRIGGER IF NOT EXISTS notes_au AFTER UPDATE ON notes BEGIN
UPDATE notes_fts SET content = new.content, date = new.date WHERE rowid = old.id;
END;
'''; ''';
static Future<String> _getDatabasePath() async { static Future<String> _getDatabasePath() async {
@@ -44,7 +67,7 @@ CREATE TABLE IF NOT EXISTS settings (
if (home == null) { if (home == null) {
throw Exception('Could not find home directory'); throw Exception('Could not find home directory');
} }
debugPrint('Home directory found: home'); debugPrint('Home directory found: $home');
final dbDir = Directory(path.join(home, settingsDir)); final dbDir = Directory(path.join(home, settingsDir));
if (!await dbDir.exists()) { if (!await dbDir.exists()) {
@@ -58,7 +81,7 @@ CREATE TABLE IF NOT EXISTS settings (
} else { } else {
// Default path for other platforms // Default path for other platforms
final databasesPath = await databaseFactoryFfi.getDatabasesPath(); final databasesPath = await databaseFactoryFfi.getDatabasesPath();
debugPrint('Using default databases path: databasesPath'); debugPrint('Using default databases path: $databasesPath');
return path.join(databasesPath, dbFileName); return path.join(databasesPath, dbFileName);
} }
} }
@@ -68,7 +91,7 @@ CREATE TABLE IF NOT EXISTS settings (
sqfliteFfiInit(); sqfliteFfiInit();
final dbPath = await _getDatabasePath(); final dbPath = await _getDatabasePath();
debugPrint('Database path: dbPath'); debugPrint('Database path: $dbPath');
try { try {
db = await databaseFactoryFfi.openDatabase( db = await databaseFactoryFfi.openDatabase(
@@ -84,7 +107,7 @@ CREATE TABLE IF NOT EXISTS settings (
); );
debugPrint('Database opened and initialized'); debugPrint('Database opened and initialized');
} catch (e) { } catch (e) {
debugPrint('Failed to initialize database: e'); debugPrint('Failed to initialize database: $e');
rethrow; rethrow;
} }
} }
@@ -104,11 +127,73 @@ CREATE TABLE IF NOT EXISTS settings (
} }
static Future<void> setSetting(String key, String value) async { static Future<void> setSetting(String key, String value) async {
await db.insert( await db.insert('settings', {
'settings', 'key': key,
{'key': key, 'value': value}, 'value': value,
conflictAlgorithm: ConflictAlgorithm.replace, }, conflictAlgorithm: ConflictAlgorithm.replace);
);
debugPrint("Setting updated: $key = $value"); debugPrint("Setting updated: $key = $value");
} }
// Search notes using FTS
static Future<List<Map<String, dynamic>>> searchNotes(String query) async {
try {
if (query.trim().isEmpty) {
return [];
}
// Process the query for partial word matching
// Split into individual terms, filter empty ones
List<String> terms =
query
.trim()
.split(RegExp(r'\s+'))
.where((term) => term.isNotEmpty)
.toList();
if (terms.isEmpty) {
return [];
}
// Add wildcards to each term for prefix matching (e.g., "fuck*" will match "fucked")
// Join terms with AND for all-term matching (results must contain ALL terms)
String ftsQuery = terms
.map((term) {
// Remove any special characters that might break the query
String sanitizedTerm = term.replaceAll(RegExp(r'[^\w]'), '');
if (sanitizedTerm.isEmpty) return '';
// Add wildcard for stemming/prefix matching
return '$sanitizedTerm*';
})
.where((term) => term.isNotEmpty)
.join(' AND ');
if (ftsQuery.isEmpty) {
debugPrint('Query was sanitized to empty string');
return [];
}
debugPrint('FTS query: "$ftsQuery"');
// Execute the FTS query with AND logic
final List<Map<String, dynamic>> results = await db.rawQuery(
'''
SELECT n.id, n.date, n.content, snippet(notes_fts, 0, '<b>', '</b>', '...', 20) as snippet
FROM notes_fts
JOIN notes n ON notes_fts.rowid = n.id
WHERE notes_fts MATCH ?
ORDER BY n.date DESC
LIMIT 100
''',
[ftsQuery],
);
debugPrint('Search query "$ftsQuery" returned ${results.length} results');
return results;
} catch (e) {
debugPrint('Search failed: $e');
// Return empty results rather than crashing on malformed queries
return [];
}
}
} }

View File

@@ -7,8 +7,10 @@ import 'package:system_tray/system_tray.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import 'package:audioplayers/audioplayers.dart'; import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'dart:math';
// TODO: Sound does not play when ran from a different workdir? Weird // 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 // Default values - will be replaced by DB values if they exist
const Duration _defaultPopupInterval = Duration(minutes: 20); const Duration _defaultPopupInterval = Duration(minutes: 20);
@@ -127,6 +129,7 @@ class MainPageState extends State<MainPage> with WindowListener {
final SystemTray _systemTray = SystemTray(); final SystemTray _systemTray = SystemTray();
final Menu _menu = Menu(); final Menu _menu = Menu();
final AudioPlayer _audioPlayer = AudioPlayer(); final AudioPlayer _audioPlayer = AudioPlayer();
double _volume = 0.7; // Default volume level (0.0 to 1.0)
final TextEditingController _previousEntryController = final TextEditingController _previousEntryController =
TextEditingController(); TextEditingController();
@@ -135,6 +138,7 @@ class MainPageState extends State<MainPage> with WindowListener {
final TextEditingController _scratchController = TextEditingController(); final TextEditingController _scratchController = TextEditingController();
final TextEditingController _intervalController = TextEditingController(); final TextEditingController _intervalController = TextEditingController();
final TextEditingController _soundController = TextEditingController(); final TextEditingController _soundController = TextEditingController();
final TextEditingController _searchController = TextEditingController();
Note? previousNote; Note? previousNote;
Note? _currentlyDisplayedNote; Note? _currentlyDisplayedNote;
@@ -143,9 +147,12 @@ class MainPageState extends State<MainPage> with WindowListener {
bool _canGoPrevious = false; bool _canGoPrevious = false;
bool _canGoNext = false; bool _canGoNext = false;
bool _isSearching = false;
List<Note> _searchResults = [];
Timer? _popupTimer; Timer? _popupTimer;
Timer? _debounceTimer; Timer? _debounceTimer;
Timer? _searchDebounceTimer;
@override @override
void initState() { void initState() {
@@ -153,6 +160,7 @@ class MainPageState extends State<MainPage> with WindowListener {
windowManager.addListener(this); windowManager.addListener(this);
_initSystemTray(); _initSystemTray();
_loadData(); _loadData();
_loadVolume();
windowManager.setPreventClose(true); windowManager.setPreventClose(true);
_setWindowConfig(); _setWindowConfig();
} }
@@ -162,12 +170,14 @@ class MainPageState extends State<MainPage> with WindowListener {
windowManager.removeListener(this); windowManager.removeListener(this);
_popupTimer?.cancel(); _popupTimer?.cancel();
_debounceTimer?.cancel(); _debounceTimer?.cancel();
_searchDebounceTimer?.cancel();
_previousEntryController.dispose(); _previousEntryController.dispose();
_currentEntryController.dispose(); _currentEntryController.dispose();
_currentEntryFocusNode.dispose(); _currentEntryFocusNode.dispose();
_scratchController.dispose(); _scratchController.dispose();
_intervalController.dispose(); _intervalController.dispose();
_soundController.dispose(); _soundController.dispose();
_searchController.dispose();
_audioPlayer.dispose(); _audioPlayer.dispose();
super.dispose(); super.dispose();
} }
@@ -212,7 +222,9 @@ class MainPageState extends State<MainPage> with WindowListener {
_popupTimer = Timer.periodic(_currentPopupInterval, (timer) { _popupTimer = Timer.periodic(_currentPopupInterval, (timer) {
_showWindow(); _showWindow();
}); });
debugPrint("Popup timer started with interval: ${_currentPopupInterval.inMinutes} minutes"); debugPrint(
"Popup timer started with interval: ${_currentPopupInterval.inMinutes} minutes",
);
} }
Future<void> _showWindow() async { Future<void> _showWindow() async {
@@ -222,6 +234,8 @@ class MainPageState extends State<MainPage> with WindowListener {
await windowManager.setSize(const Size(1600, 900)); await windowManager.setSize(const Size(1600, 900));
await windowManager.center(); await windowManager.center();
await windowManager.show(); await windowManager.show();
await windowManager.focus();
_currentEntryFocusNode.requestFocus();
await _playSound(); await _playSound();
} else { } else {
await windowManager.focus(); await windowManager.focus();
@@ -229,11 +243,25 @@ class MainPageState extends State<MainPage> with WindowListener {
} }
} }
// 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 { Future<void> _playSound() async {
await _audioPlayer.stop(); await _audioPlayer.stop();
try { try {
// Set volume before playing (convert linear slider value to log volume)
await _audioPlayer.setVolume(_linearToLogVolume(_volume));
await _audioPlayer.play(AssetSource('sounds/$_currentNotificationSound')); await _audioPlayer.play(AssetSource('sounds/$_currentNotificationSound'));
debugPrint("Played sound: $_currentNotificationSound"); debugPrint(
"Played sound: $_currentNotificationSound at volume: $_volume (log: ${_linearToLogVolume(_volume)})",
);
} catch (e) { } catch (e) {
debugPrint("Error playing sound $_currentNotificationSound: $e"); debugPrint("Error playing sound $_currentNotificationSound: $e");
} }
@@ -287,7 +315,9 @@ class MainPageState extends State<MainPage> with WindowListener {
String? intervalMinutesStr = await DB.getSetting('popupIntervalMinutes'); String? intervalMinutesStr = await DB.getSetting('popupIntervalMinutes');
String? soundFileStr = await DB.getSetting('notificationSound'); String? soundFileStr = await DB.getSetting('notificationSound');
int intervalMinutes = int.tryParse(intervalMinutesStr ?? '') ?? _defaultPopupInterval.inMinutes; int intervalMinutes =
int.tryParse(intervalMinutesStr ?? '') ??
_defaultPopupInterval.inMinutes;
_currentPopupInterval = Duration(minutes: intervalMinutes); _currentPopupInterval = Duration(minutes: intervalMinutes);
_currentNotificationSound = soundFileStr ?? _defaultNotificationSound; _currentNotificationSound = soundFileStr ?? _defaultNotificationSound;
@@ -310,6 +340,25 @@ class MainPageState extends State<MainPage> with WindowListener {
debugPrint("Data loaded."); 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");
}
void _saveData() async { void _saveData() async {
String previousEntry = _previousEntryController.text; String previousEntry = _previousEntryController.text;
String currentEntry = _currentEntryController.text; String currentEntry = _currentEntryController.text;
@@ -324,7 +373,8 @@ class MainPageState extends State<MainPage> with WindowListener {
await updateNote(previousNote!); await updateNote(previousNote!);
} }
int newIntervalMinutes = int.tryParse(intervalStr) ?? _currentPopupInterval.inMinutes; int newIntervalMinutes =
int.tryParse(intervalStr) ?? _currentPopupInterval.inMinutes;
Duration newInterval = Duration(minutes: newIntervalMinutes); Duration newInterval = Duration(minutes: newIntervalMinutes);
if (newInterval != _currentPopupInterval) { if (newInterval != _currentPopupInterval) {
_currentPopupInterval = newInterval; _currentPopupInterval = newInterval;
@@ -341,6 +391,9 @@ class MainPageState extends State<MainPage> with WindowListener {
DB.setSetting('notificationSound', soundStr); DB.setSetting('notificationSound', soundStr);
} }
// Also save volume
await _saveVolume();
debugPrint( debugPrint(
"Saving data... Current Entry: [${currentEntry.length} chars], Scratch: [${scratchContent.length} chars]", "Saving data... Current Entry: [${currentEntry.length} chars], Scratch: [${scratchContent.length} chars]",
); );
@@ -350,29 +403,294 @@ class MainPageState extends State<MainPage> with WindowListener {
await windowManager.setAspectRatio(16 / 9); 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);
// Important: update the dialog state after search completes
dialogSetState(() {
_searchResults = results;
_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.date,
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: () {
Navigator.of(context).pop();
this.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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Wrap Scaffold with RawKeyboardListener as workaround for Escape key // Wrap Scaffold with RawKeyboardListener as workaround for Escape key
return RawKeyboardListener( return RawKeyboardListener(
focusNode: FocusNode(), // Needs its own node focusNode:
FocusNode()
..requestFocus(), // Request focus to ensure keyboard events are captured
onKey: (RawKeyEvent event) { onKey: (RawKeyEvent event) {
if (event is RawKeyDownEvent && if (event is RawKeyDownEvent) {
event.logicalKey == LogicalKeyboardKey.escape) { // Handle Escape to close window
debugPrint( if (event.logicalKey == LogicalKeyboardKey.escape) {
"Escape pressed inside MainPage (RawKeyboardListener - Workaround)", debugPrint(
); "Escape pressed inside MainPage (RawKeyboardListener - Workaround)",
// Call method directly since we are in the state );
FocusManager.instance.primaryFocus?.unfocus(); // Keep unfocus attempt // Call method directly since we are in the state
onWindowClose(); 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( child: Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Journaler'), title: const Text('Journaler'),
actions: <Widget>[ actions: <Widget>[
// Add search button
IconButton(
icon: const Icon(Icons.search),
tooltip: 'Search Notes',
onPressed: _showSearchDialog,
),
// Group Label and Input for Interval // Group Label and Input for Interval
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0).copyWith(left: 8.0), // Add padding padding: const EdgeInsets.symmetric(
vertical: 8.0,
).copyWith(left: 8.0), // Add padding
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, // Use minimum space mainAxisSize: MainAxisSize.min, // Use minimum space
children: [ children: [
@@ -386,11 +704,14 @@ class MainPageState extends State<MainPage> with WindowListener {
decoration: const InputDecoration( decoration: const InputDecoration(
border: OutlineInputBorder(), border: OutlineInputBorder(),
isDense: true, isDense: true,
contentPadding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0), contentPadding: EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 8.0,
),
), ),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
inputFormatters: <TextInputFormatter>[ inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.digitsOnly FilteringTextInputFormatter.digitsOnly,
], ],
), ),
), ),
@@ -399,7 +720,10 @@ class MainPageState extends State<MainPage> with WindowListener {
), ),
// Group Label and Input for Sound // Group Label and Input for Sound
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 8.0), padding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 8.0,
),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@@ -413,7 +737,10 @@ class MainPageState extends State<MainPage> with WindowListener {
decoration: const InputDecoration( decoration: const InputDecoration(
border: OutlineInputBorder(), border: OutlineInputBorder(),
isDense: true, isDense: true,
contentPadding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0), contentPadding: EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 8.0,
),
hintText: 'sound.mp3', hintText: 'sound.mp3',
), ),
), ),
@@ -421,6 +748,11 @@ class MainPageState extends State<MainPage> with WindowListener {
], ],
), ),
), ),
// Volume Control Slider
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: _buildVolumeSlider(),
),
// Test Sound Button // Test Sound Button
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0), padding: const EdgeInsets.symmetric(vertical: 8.0),
@@ -463,16 +795,21 @@ class MainPageState extends State<MainPage> with WindowListener {
children: [ children: [
Expanded( Expanded(
child: Text( child: Text(
_currentlyDisplayedNote?.date == previousNote?.date _currentlyDisplayedNote?.date ==
previousNote?.date
? 'Previous Entry (Latest)' ? 'Previous Entry (Latest)'
: 'Entry: ${_currentlyDisplayedNote?.date ?? 'N/A'}', : 'Entry: ${_currentlyDisplayedNote?.date ?? 'N/A'}',
style: TextStyle(fontSize: 18, color: Colors.grey), style: TextStyle(
fontSize: 18,
color: Colors.grey,
),
), ),
), ),
IconButton( IconButton(
icon: const Icon(Icons.arrow_back), icon: const Icon(Icons.arrow_back),
tooltip: 'Previous Note', tooltip: 'Previous Note',
onPressed: _canGoPrevious ? _goToPreviousNote : null, onPressed:
_canGoPrevious ? _goToPreviousNote : null,
), ),
IconButton( IconButton(
icon: const Icon(Icons.arrow_forward), icon: const Icon(Icons.arrow_forward),
@@ -484,19 +821,27 @@ class MainPageState extends State<MainPage> with WindowListener {
Expanded( Expanded(
child: TextField( child: TextField(
controller: _previousEntryController, controller: _previousEntryController,
readOnly: _currentlyDisplayedNote?.date != previousNote?.date, readOnly:
_currentlyDisplayedNote?.date !=
previousNote?.date,
maxLines: null, maxLines: null,
expands: true, expands: true,
style: Theme.of(context).textTheme.bodyMedium, style: Theme.of(context).textTheme.bodyMedium,
decoration: InputDecoration( decoration: InputDecoration(
hintText: _currentlyDisplayedNote?.date != previousNote?.date hintText:
? 'Viewing note from ${_currentlyDisplayedNote?.date} (Read-Only)' _currentlyDisplayedNote?.date !=
: 'Latest Note', previousNote?.date
? 'Viewing note from ${_currentlyDisplayedNote?.date} (Read-Only)'
: 'Latest Note',
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
filled: _currentlyDisplayedNote?.date != previousNote?.date, filled:
fillColor: _currentlyDisplayedNote?.date != previousNote?.date _currentlyDisplayedNote?.date !=
? Colors.grey.withOpacity(0.1) previousNote?.date,
: null, fillColor:
_currentlyDisplayedNote?.date !=
previousNote?.date
? Colors.grey.withOpacity(0.1)
: null,
), ),
), ),
), ),
@@ -525,10 +870,11 @@ class MainPageState extends State<MainPage> with WindowListener {
controller: _scratchController, controller: _scratchController,
maxLines: null, maxLines: null,
expands: true, expands: true,
style: Theme.of(context).textTheme.bodyMedium, // Apply theme text style style:
decoration: const InputDecoration( Theme.of(
labelText: 'Scratch', context,
), ).textTheme.bodyMedium, // Apply theme text style
decoration: const InputDecoration(labelText: 'Scratch'),
), ),
), ),
], ],

View File

@@ -3,8 +3,10 @@ import 'package:journaler/db.dart';
class Note { class Note {
final String date; final String date;
String content; String content;
String?
snippet; // Optional field to hold highlighted snippets for search results
Note({required this.date, required this.content}); Note({required this.date, required this.content, this.snippet});
} }
class Scratch { class Scratch {
@@ -100,3 +102,25 @@ Future<Note?> getNextNote(String currentDate) async {
// This handles the case where the `currentDate` might not be the absolute latest. // This handles the case where the `currentDate` might not be the absolute latest.
return getLatestNote(); return getLatestNote();
} }
// Search notes using full-text search
Future<List<Note>> searchNotes(String query) async {
if (query.isEmpty) {
return [];
}
// Call DB search function
final List<Map<String, dynamic>> results = await DB.searchNotes(query);
// Convert results to Note objects
return results
.map(
(result) => Note(
date: result['date'] as String,
content: result['content'] as String,
snippet:
result['snippet'] as String?, // Highlighted snippets from FTS
),
)
.toList();
}