Skip to content

Commit 74de4e6

Browse files
committed
feat: enhance database FTS functionality and improve backup settings UI
1 parent afcc756 commit 74de4e6

11 files changed

Lines changed: 351 additions & 134 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 2.9.1 - 15/06/2026
2+
3+
Fix bugs with searching poems for some users.
4+
15
## 2.9.0 - 01/06/2026
26

37
- Create your own templates for image generation!

lib/database/database.dart

Lines changed: 72 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,27 @@ class Database extends _$Database {
3535
onCreate: (m) async {
3636
await m.createAll();
3737

38-
// Create the FTS table and triggers on a fwresh install so search works
38+
// Create the FTS table and triggers on a fresh install so search works
3939
await _createFTS5();
4040
await _prepopulateDefaultTemplates();
4141
},
4242
beforeOpen: (details) async {
4343
await customStatement('PRAGMA foreign_keys = ON');
44+
// Only recreate triggers and rebuild if the legacy triggers
45+
// (using direct UPDATE) are found.
46+
// This ensures we only run the expensive FTS rebuild
47+
// once to repair corrupted databases.
48+
final triggerRow = await customSelect(
49+
"SELECT sql FROM sqlite_master WHERE type='trigger' "
50+
"AND name='poem_fts_update';",
51+
).getSingleOrNull();
52+
53+
if (triggerRow != null) {
54+
final triggerSql = triggerRow.read<String>('sql');
55+
if (triggerSql.contains('UPDATE poem_fts')) {
56+
await _recreateTriggersAndRebuildFTS();
57+
}
58+
}
4459
},
4560
onUpgrade: stepByStep(
4661
from1To2: (m, schema) async {
@@ -69,26 +84,24 @@ class Database extends _$Database {
6984
);
7085

7186
Future<List<PoemModel>> searchPoems(String query) async {
72-
final ftsQuery = '$query*';
73-
final hasDeletedAt = await _poemHasDeletedAt();
74-
75-
final whereClause = hasDeletedAt
76-
? 'WHERE p.deleted_at IS NULL AND poem_fts MATCH ?'
77-
: 'WHERE poem_fts MATCH ?';
87+
String sanitized = query.trim();
88+
if (sanitized.endsWith('*')) {
89+
sanitized = sanitized.substring(0, sanitized.length - 1).trim();
90+
}
91+
if (sanitized.isEmpty) {
92+
return [];
93+
}
94+
final escapedQuery = sanitized.replaceAll('"', '""');
95+
final ftsQuery = '"$escapedQuery"*';
7896

7997
return customSelect(
8098
'SELECT p.* FROM poem as p JOIN poem_fts as fts ON p.id = fts.rowid '
81-
'$whereClause ORDER BY rank LIMIT 10',
99+
'WHERE p.deleted_at IS NULL AND poem_fts MATCH ? ORDER BY rank LIMIT 10',
82100
variables: [Variable<String>(ftsQuery)],
83101
readsFrom: {poem},
84102
).map((row) => PoemModel.fromJson(row.data)).get();
85103
}
86104

87-
Future<bool> _poemHasDeletedAt() async {
88-
final columns = await customSelect('PRAGMA table_info(poem);').get();
89-
return columns.any((row) => row.data['name'] == 'deleted_at');
90-
}
91-
92105
Stream<List<PoemModel>> get getPoemStream =>
93106
(select(poem)
94107
..where((tbl) => tbl.deletedAt.isNull())
@@ -197,17 +210,55 @@ class Database extends _$Database {
197210
// Trigger to sync FTS table on UPDATE
198211
await customStatement('''
199212
CREATE TRIGGER poem_fts_update AFTER UPDATE ON poem BEGIN
200-
UPDATE poem_fts SET title = new.title, poem = new.poem
201-
WHERE rowid = new.id;
213+
INSERT INTO poem_fts(poem_fts, rowid, title, poem)
214+
VALUES ('delete', old.id, old.title, old.poem);
215+
INSERT INTO poem_fts(rowid, title, poem)
216+
VALUES (new.id, new.title, new.poem);
202217
END;
203218
''');
204219

205220
// Trigger to sync FTS table on DELETE
206221
await customStatement('''
207222
CREATE TRIGGER poem_fts_delete AFTER DELETE ON poem BEGIN
208-
DELETE FROM poem_fts WHERE rowid = old.id;
223+
INSERT INTO poem_fts(poem_fts, rowid, title, poem)
224+
VALUES ('delete', old.id, old.title, old.poem);
225+
END;
226+
''');
227+
});
228+
}
229+
230+
Future<void> _recreateTriggersAndRebuildFTS() async {
231+
await transaction(() async {
232+
await customStatement('DROP TRIGGER IF EXISTS poem_fts_insert;');
233+
await customStatement('DROP TRIGGER IF EXISTS poem_fts_update;');
234+
await customStatement('DROP TRIGGER IF EXISTS poem_fts_delete;');
235+
236+
await customStatement('''
237+
CREATE TRIGGER poem_fts_insert AFTER INSERT ON poem BEGIN
238+
INSERT INTO poem_fts(rowid, title, poem)
239+
VALUES (new.id, new.title, new.poem);
240+
END;
241+
''');
242+
243+
await customStatement('''
244+
CREATE TRIGGER poem_fts_update AFTER UPDATE ON poem BEGIN
245+
INSERT INTO poem_fts(poem_fts, rowid, title, poem)
246+
VALUES ('delete', old.id, old.title, old.poem);
247+
INSERT INTO poem_fts(rowid, title, poem)
248+
VALUES (new.id, new.title, new.poem);
249+
END;
250+
''');
251+
252+
await customStatement('''
253+
CREATE TRIGGER poem_fts_delete AFTER DELETE ON poem BEGIN
254+
INSERT INTO poem_fts(poem_fts, rowid, title, poem)
255+
VALUES ('delete', old.id, old.title, old.poem);
209256
END;
210257
''');
258+
259+
await customStatement(
260+
'INSERT INTO poem_fts(poem_fts) VALUES(\'rebuild\');',
261+
);
211262
});
212263
}
213264

@@ -246,7 +297,11 @@ class Database extends _$Database {
246297
];
247298

248299
await batch((batch) {
249-
batch.insertAll(templates, defaultTemplates);
300+
batch.insertAll(
301+
templates,
302+
defaultTemplates,
303+
mode: InsertMode.insertOrReplace,
304+
);
250305
});
251306
}
252307

lib/screens/backup_setting_screen/backup_setting_screen.dart

Lines changed: 49 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -52,65 +52,60 @@ Click to login to Google and enable drive backup."""),
5252
);
5353
}
5454

55-
return BaseInfoWidget(
56-
title: "BACKUP",
57-
children: [
58-
ListTile(
59-
title: const Text("Backup Email"),
60-
subtitle: Text(model.backupEmail!),
61-
trailing: TextButton(
62-
child: const Text("Sign Out"),
63-
onPressed: () {
64-
ref.read(authProvider.notifier).signOut();
65-
},
66-
),
67-
),
68-
SwitchListTile(
69-
value: model.isAutoBackupEnabled,
70-
title: const Text(
71-
"Enable Auto backup to Google Drive",
72-
),
73-
subtitle: const Text(
74-
"Enable auto backup your data to Google Drive. "
75-
"You can restore your data on another device.",
76-
),
77-
onChanged: (value) {
78-
ref
79-
.read(configProvider.notifier)
80-
.isAutoBackupEnabled =
81-
value;
55+
return BaseInfoWidget(
56+
title: "BACKUP",
57+
children: [
58+
ListTile(
59+
title: const Text("Backup Email"),
60+
subtitle: Text(model.backupEmail!),
61+
trailing: TextButton(
62+
child: const Text("Sign Out"),
63+
onPressed: () {
64+
ref.read(authProvider.notifier).signOut();
8265
},
8366
),
84-
const Divider(),
85-
ListTile(
86-
title: const Text("Backup Now"),
87-
subtitle: const Text(
88-
"Backup your data to Google Drive immediately.",
89-
),
90-
enabled: model.backupEmail != null,
91-
onTap: () =>
92-
ref //
93-
.read(backupManagerProvider.notifier)
94-
.backup(forceBackup: true),
67+
),
68+
SwitchListTile(
69+
value: model.isAutoBackupEnabled,
70+
title: const Text("Enable Auto backup to Google Drive"),
71+
subtitle: const Text(
72+
"Enable auto backup your data to Google Drive. "
73+
"You can restore your data on another device.",
9574
),
96-
ListTile(
97-
title: const Text("Last Backup"),
98-
subtitle: model.lastBackup == null
99-
? const Text("Have no backup yet.")
100-
: Text(
101-
dateTimeFormatter.format(model.lastBackup!),
102-
),
75+
onChanged: (value) {
76+
ref
77+
.read(configProvider.notifier)
78+
.isAutoBackupEnabled =
79+
value;
80+
},
81+
),
82+
const Divider(),
83+
ListTile(
84+
title: const Text("Backup Now"),
85+
subtitle: const Text(
86+
"Backup your data to Google Drive immediately.",
10387
),
104-
],
105-
);
106-
},
107-
loading: () =>
108-
const Center(child: CircularProgressIndicator()),
109-
error: (e, s) => Center(child: Text("Error: $e\n$s")),
110-
),
111-
],
112-
),
88+
enabled: model.backupEmail != null,
89+
onTap: () =>
90+
ref //
91+
.read(backupManagerProvider.notifier)
92+
.backup(forceBackup: true),
93+
),
94+
ListTile(
95+
title: const Text("Last Backup"),
96+
subtitle: model.lastBackup == null
97+
? const Text("Have no backup yet.")
98+
: Text(dateTimeFormatter.format(model.lastBackup!)),
99+
),
100+
],
101+
);
102+
},
103+
loading: () => const Center(child: CircularProgressIndicator()),
104+
error: (e, s) => Center(child: Text("Error: $e\n$s")),
105+
),
106+
],
113107
),
108+
),
114109
bottomNavigationBar: isIOS ? const OnlyBackButtonBottomAppBar() : null,
115110
),
116111
);

lib/screens/image_screen/image_screen.dart

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import 'dart:io';
44
import 'package:flutter/material.dart';
55
import 'package:image_picker/image_picker.dart';
66
import 'package:material_symbols_icons/symbols.dart';
7-
import 'package:path/path.dart';
7+
import 'package:path/path.dart' as path;
88
import 'package:path_provider/path_provider.dart';
99
import 'package:screenshot/screenshot.dart';
1010
import 'package:share_plus/share_plus.dart';
@@ -118,7 +118,7 @@ class _ImageScreenState extends State<ImageScreen> {
118118
await _loadTemplates();
119119

120120
if (mounted) {
121-
ScaffoldMessenger.of(this.context).showSnackBar(
121+
ScaffoldMessenger.of(context).showSnackBar(
122122
SnackBar(
123123
content: Text(
124124
'Template "${selectedTemplate!.name}" updated successfully!',
@@ -380,14 +380,26 @@ class _ImageScreenState extends State<ImageScreen> {
380380
}
381381

382382
Future<void> _shareAll(List<String> images) async {
383-
await SharePlus.instance.share(
384-
ShareParams(
385-
text:
386-
"Made using Heartry 💜\n"
387-
"Download at https://play.google.com/store/apps/details?id=com.darshan.heartry",
388-
files: images.map((img) => XFile(img, mimeType: "image/png")).toList(),
389-
),
390-
);
383+
try {
384+
await SharePlus.instance.share(
385+
ShareParams(
386+
text:
387+
"Made using Heartry 💜\n"
388+
"Download at https://play.google.com/store/apps/details?id=com.darshan.heartry",
389+
files: images
390+
.map((img) => XFile(img, mimeType: "image/png"))
391+
.toList(),
392+
),
393+
);
394+
} catch (e) {
395+
if (mounted) {
396+
ScaffoldMessenger.of(context).showSnackBar(
397+
SnackBar(content: Text("Sharing failed: ${e.toString()}")),
398+
);
399+
}
400+
401+
rethrow;
402+
}
391403
}
392404

393405
void _showProgressDialog(BuildContext context) {
@@ -410,14 +422,14 @@ class _ImageScreenState extends State<ImageScreen> {
410422

411423
final time = DateTime.now().toIso8601String();
412424

413-
final path = join(tmpDir.path, "${widget.title}-$time-$index.png");
425+
final p = path.join(tmpDir.path, "${widget.title}-$time-$index.png");
414426

415427
final imgBytes = await _screenshot.capture(
416-
pixelRatio: 3,
428+
pixelRatio: 2,
417429
delay: const Duration(milliseconds: 100),
418430
);
419431

420-
final imgFile = File(path)..writeAsBytes(imgBytes!);
432+
final imgFile = File(p)..writeAsBytes(imgBytes!);
421433

422434
return imgFile.path;
423435
}

0 commit comments

Comments
 (0)