diff --git a/lib/db.dart b/lib/db.dart index c95f4d1..a6b12fb 100644 --- a/lib/db.dart +++ b/lib/db.dart @@ -360,11 +360,12 @@ END; } // Split into individual terms, filter empty ones - List terms = query - .trim() - .split(RegExp(r'\s+')) - .where((term) => term.isNotEmpty) - .toList(); + List terms = + query + .trim() + .split(RegExp(r'\s+')) + .where((term) => term.isNotEmpty) + .toList(); if (terms.isEmpty) { return []; @@ -376,7 +377,7 @@ END; // Remove dangerous characters but preserve Unicode String sanitizedTerm = term.replaceAll(RegExp(r'''['"]'''), ''); if (sanitizedTerm.isEmpty) return ''; - + // Use proper FTS5 syntax: each word becomes a separate token with prefix matching List words = sanitizedTerm.split(RegExp(r'\s+')); return words.map((word) => '$word*').join(' OR '); diff --git a/lib/main.dart b/lib/main.dart index 6cfbfa5..564b5b0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -575,6 +575,41 @@ class MainPageState extends State with WindowListener { debugPrint("Volume saved: $_volume"); } + // Check if content appears to be keyboard smashing or repeated characters + bool _isLikelyKeyboardSmashing(String content) { + // Skip empty content + if (content.trim().isEmpty) return false; + + // Check for repeated characters (like "wwwwwwww") + final repeatedCharPattern = RegExp( + r'(.)\1{4,}', + ); // 5 or more repeated chars + if (repeatedCharPattern.hasMatch(content)) { + // But allow if it's a legitimate pattern like base64 + if (RegExp(r'^[A-Za-z0-9+/=]+$').hasMatch(content)) return false; + return true; + } + + // Check for alternating characters (like "asdfasdf") + final alternatingPattern = RegExp(r'(.)(.)\1\2{2,}'); + if (alternatingPattern.hasMatch(content)) return true; + + // Check for common keyboard smashing patterns + final commonPatterns = [ + r'[qwerty]{5,}', // Common keyboard rows + r'[asdf]{5,}', + r'[zxcv]{5,}', + r'[1234]{5,}', + r'[wasd]{5,}', // Common gaming keys + ]; + + for (final pattern in commonPatterns) { + if (RegExp(pattern, caseSensitive: false).hasMatch(content)) return true; + } + + return false; + } + Future _saveData() async { String previousEntry = _previousEntryController.text; String currentEntry = _currentEntryController.text; @@ -584,8 +619,14 @@ class MainPageState extends State with WindowListener { // Handle current entry if (currentEntry.isNotEmpty) { - await createNote(currentEntry); - _currentEntryController.clear(); // Clear the input field after saving + // Skip saving if it looks like keyboard smashing + if (!_isLikelyKeyboardSmashing(currentEntry)) { + await createNote(currentEntry); + _currentEntryController.clear(); // Clear the input field after saving + } else { + debugPrint("Skipping save of likely keyboard smashing: $currentEntry"); + _currentEntryController.clear(); // Still clear the input + } } // Handle scratch pad @@ -756,7 +797,9 @@ class MainPageState extends State with WindowListener { .toList(); // Sort by date, newest first - filteredResults.sort((a, b) => b.date.compareTo(a.date)); + filteredResults.sort( + (a, b) => b.date.compareTo(a.date), + ); // Important: update the dialog state after search completes dialogSetState(() { @@ -908,6 +951,143 @@ class MainPageState extends State with WindowListener { ); } + // Show cleanup dialog + void _showCleanupDialog() async { + final problematicEntries = await findProblematicEntries(); + + if (!mounted) return; + + showDialog( + context: context, + builder: (BuildContext context) { + return StatefulBuilder( + builder: (context, dialogSetState) { + return AlertDialog( + title: const Text('Cleanup Problematic Entries'), + content: SizedBox( + width: MediaQuery.of(context).size.width * 0.7, + height: MediaQuery.of(context).size.height * 0.7, + child: Column( + children: [ + Text( + 'Found ${problematicEntries.length} potentially problematic entries', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + Expanded( + child: problematicEntries.isEmpty + ? const Center( + child: Text('No problematic entries found!'), + ) + : ListView.builder( + itemCount: problematicEntries.length, + itemBuilder: (context, index) { + final note = problematicEntries[index]; + return Card( + margin: const EdgeInsets.only(bottom: 8), + child: ListTile( + title: Text( + note.displayDate, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Reason: ${note.problemReason}', + style: TextStyle( + color: Colors.red[700], + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text(note.content), + ], + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.visibility), + tooltip: 'View in main window', + onPressed: () { + // Close the dialog + Navigator.of(context).pop(); + // Navigate to the note in main window + setState(() { + _currentlyDisplayedNote = note; + _previousEntryController.text = note.content; + }); + _checkNavigation(); + }, + ), + IconButton( + icon: const Icon(Icons.delete), + color: Colors.red, + tooltip: 'Delete (hold Shift to skip confirmation)', + onPressed: () async { + // Check if shift is pressed + final isShiftPressed = HardwareKeyboard.instance.isShiftPressed; + + bool shouldDelete = isShiftPressed; + if (!isShiftPressed) { + // Show confirmation dialog + shouldDelete = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Delete Entry?'), + content: const Text( + 'Are you sure you want to delete this entry? This action cannot be undone.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: const Text('Delete'), + ), + ], + ), + ) ?? false; + } + + if (shouldDelete) { + await deleteNote(note.date); + dialogSetState(() { + problematicEntries.removeAt(index); + }); + } + }, + ), + ], + ), + ), + ); + }, + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Close'), + ), + ], + ); + }, + ); + }, + ); + } + @override Widget build(BuildContext context) { // Wrap Scaffold with RawKeyboardListener as workaround for Escape key @@ -939,6 +1119,12 @@ class MainPageState extends State with WindowListener { appBar: AppBar( title: const Text('Journaler'), actions: [ + // Add cleanup button + IconButton( + icon: const Icon(Icons.cleaning_services), + tooltip: 'Cleanup Problematic Entries', + onPressed: _showCleanupDialog, + ), // Add search button IconButton( icon: const Icon(Icons.search), diff --git a/lib/notes.dart b/lib/notes.dart index 67e15aa..2b4a84f 100644 --- a/lib/notes.dart +++ b/lib/notes.dart @@ -6,8 +6,16 @@ class Note { late final String displayDate; String content; String? snippet; + bool isProblematic; + String problemReason; - Note({required this.date, required this.content, this.snippet}) { + Note({ + required this.date, + required this.content, + this.snippet, + this.isProblematic = false, + this.problemReason = '', + }) { final dtUtc = DateFormat('yyyy-MM-dd HH:mm:ss').parse(date, true); final dtLocal = dtUtc.toLocal(); displayDate = DateFormat('yyyy-MM-dd HH:mm:ss').format(dtLocal); @@ -152,3 +160,36 @@ Future> searchNotes(String query) async { ) .toList(); } + +// Find potentially problematic entries based on character distribution +Future> findProblematicEntries() async { + const double maxCharPercentage = 0.7; // If a single char makes up more than 70%, it's suspicious + const int minLength = 10; // Only check notes longer than this + + // Simple SQLite query that counts character occurrences using replace + final List> results = await DB.db.rawQuery(''' + WITH char_counts AS ( + SELECT + id, + date, + content, + substr(content, 1, 1) as char, + (length(content) - length(replace(content, substr(content, 1, 1), ''))) as char_count, + length(content) as total_length, + cast(length(content) - length(replace(content, substr(content, 1, 1), '')) as float) / length(content) as percentage + FROM notes + WHERE length(content) >= ? + ) + SELECT * + FROM char_counts + WHERE percentage > ? + ORDER BY date DESC + ''', [minLength, maxCharPercentage]); + + return results.map((row) => Note( + date: row['date'] as String, + content: row['content'] as String, + isProblematic: true, + problemReason: 'Character "${row['char']}" makes up ${(row['percentage'] * 100).toStringAsFixed(1)}% of the content', + )).toList(); +}