feat(sieve): local email filters alongside server filters (#90)
Reuse the same Sieve UI for both server-side (ManageSieve/JMAP) and local email filters. Both filter sets are stored and managed independently. Changes: - Add LocalSieveScripts table (DB schema v29) to store local Sieve scripts - Add LocalSieveRepository with full CRUD and activate-script support - Add isLocal param to SieveScriptsScreen and SieveScriptEditScreen; each screen shows a banner explaining whether scripts run on the server or device - Add routes /accounts/:id/sieve/local and /accounts/:id/sieve/local/edit - Split "Email filters" account menu entry into "Server email filters" and "Local email filters" (local is always available, server requires ManageSieve) - Wire up localSieveRepositoryProvider in DI Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
cc052db6c7
commit
0620663630
@@ -243,6 +243,16 @@ class SearchHistoryEntries extends Table {
|
||||
DateTimeColumn get searchedAt => dateTime()();
|
||||
}
|
||||
|
||||
@DataClassName('LocalSieveScriptRow')
|
||||
class LocalSieveScripts extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get accountId =>
|
||||
text().references(Accounts, #id, onDelete: KeyAction.cascade)();
|
||||
TextColumn get name => text()();
|
||||
TextColumn get content => text().withDefault(const Constant(''))();
|
||||
BoolColumn get isActive => boolean().withDefault(const Constant(false))();
|
||||
}
|
||||
|
||||
@DataClassName('UndoActionRow')
|
||||
class UndoActions extends Table {
|
||||
TextColumn get id => text()();
|
||||
@@ -273,13 +283,14 @@ class UndoActions extends Table {
|
||||
SyncHealth,
|
||||
UndoActions,
|
||||
SearchHistoryEntries,
|
||||
LocalSieveScripts,
|
||||
],
|
||||
)
|
||||
class AppDatabase extends _$AppDatabase {
|
||||
AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());
|
||||
|
||||
@override
|
||||
int get schemaVersion => 28;
|
||||
int get schemaVersion => 29;
|
||||
|
||||
Future<void> _createEmailFts() async {
|
||||
await customStatement('''
|
||||
@@ -508,6 +519,9 @@ class AppDatabase extends _$AppDatabase {
|
||||
if (from < 28) {
|
||||
await m.addColumn(emailBodies, emailBodies.mimeTreeJson);
|
||||
}
|
||||
if (from < 29) {
|
||||
await m.createTable(localSieveScripts);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
import 'package:sharedinbox/core/models/sieve_script.dart';
|
||||
import 'package:sharedinbox/data/db/database.dart';
|
||||
|
||||
class LocalSieveRepository {
|
||||
LocalSieveRepository(this._db);
|
||||
|
||||
final AppDatabase _db;
|
||||
|
||||
Future<List<SieveScript>> listScripts(String accountId) async {
|
||||
final rows = await (_db.select(_db.localSieveScripts)
|
||||
..where((t) => t.accountId.equals(accountId)))
|
||||
.get();
|
||||
return rows
|
||||
.map(
|
||||
(r) => SieveScript(
|
||||
id: r.id.toString(),
|
||||
name: r.name,
|
||||
blobId: r.id.toString(),
|
||||
isActive: r.isActive,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<String> getScriptContent(String accountId, String blobId) async {
|
||||
final rowId = int.parse(blobId);
|
||||
final row = await (_db.select(_db.localSieveScripts)
|
||||
..where(
|
||||
(t) => t.id.equals(rowId) & t.accountId.equals(accountId),
|
||||
))
|
||||
.getSingleOrNull();
|
||||
if (row == null) throw Exception('Local script not found: $blobId');
|
||||
return row.content;
|
||||
}
|
||||
|
||||
Future<SieveScript> saveScript(
|
||||
String accountId, {
|
||||
String? id,
|
||||
required String name,
|
||||
required String content,
|
||||
}) async {
|
||||
if (id != null) {
|
||||
final rowId = int.parse(id);
|
||||
await (_db.update(_db.localSieveScripts)
|
||||
..where(
|
||||
(t) => t.id.equals(rowId) & t.accountId.equals(accountId),
|
||||
))
|
||||
.write(
|
||||
LocalSieveScriptsCompanion(
|
||||
name: Value(name),
|
||||
content: Value(content),
|
||||
),
|
||||
);
|
||||
final updated = await (_db.select(_db.localSieveScripts)
|
||||
..where(
|
||||
(t) => t.id.equals(rowId) & t.accountId.equals(accountId),
|
||||
))
|
||||
.getSingleOrNull();
|
||||
return SieveScript(
|
||||
id: id,
|
||||
name: name,
|
||||
blobId: id,
|
||||
isActive: updated?.isActive ?? false,
|
||||
);
|
||||
}
|
||||
final rowId = await _db.into(_db.localSieveScripts).insert(
|
||||
LocalSieveScriptsCompanion.insert(
|
||||
accountId: accountId,
|
||||
name: name,
|
||||
content: Value(content),
|
||||
),
|
||||
);
|
||||
final idStr = rowId.toString();
|
||||
return SieveScript(id: idStr, name: name, blobId: idStr, isActive: false);
|
||||
}
|
||||
|
||||
Future<void> deleteScript(String accountId, String scriptId) async {
|
||||
final rowId = int.parse(scriptId);
|
||||
await (_db.delete(_db.localSieveScripts)
|
||||
..where(
|
||||
(t) => t.id.equals(rowId) & t.accountId.equals(accountId),
|
||||
))
|
||||
.go();
|
||||
}
|
||||
|
||||
Future<void> activateScript(String accountId, String scriptId) async {
|
||||
await _db.transaction(() async {
|
||||
await (_db.update(_db.localSieveScripts)
|
||||
..where((t) => t.accountId.equals(accountId)))
|
||||
.write(const LocalSieveScriptsCompanion(isActive: Value(false)));
|
||||
final rowId = int.parse(scriptId);
|
||||
await (_db.update(_db.localSieveScripts)
|
||||
..where(
|
||||
(t) => t.id.equals(rowId) & t.accountId.equals(accountId),
|
||||
))
|
||||
.write(const LocalSieveScriptsCompanion(isActive: Value(true)));
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user