import 'dart:io'; import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; part 'app_database.g.dart'; @DataClassName('DbCategory') class Categories extends Table { TextColumn get uuid => text().unique()(); TextColumn get encryptedName => text().named('encrypted_name')(); IntColumn get serverVersion => integer().named('server_version').withDefault(const Constant(0))(); BoolColumn get isDeleted => boolean().named('is_deleted').withDefault(const Constant(false))(); DateTimeColumn get updatedAt => dateTime().named('updated_at')(); @override Set get primaryKey => {uuid}; } @DataClassName('DbNote') class Notes extends Table { IntColumn get id => integer().autoIncrement()(); TextColumn get uuid => text().unique()(); TextColumn get title => text()(); TextColumn get body => text()(); DateTimeColumn get createdAt => dateTime().named('created_at')(); DateTimeColumn get updatedAt => dateTime().named('updated_at')(); IntColumn get sortIndex => integer().named('sort_index')(); IntColumn get serverVersion => integer().named('server_version').withDefault(const Constant(0))(); BoolColumn get isDeleted => boolean().named('is_deleted').withDefault(const Constant(false))(); TextColumn get categoryId => text().nullable().named('category_id')(); } @DriftDatabase(tables: [Notes, Categories]) class AppDatabase extends _$AppDatabase { @override int get schemaVersion => 1; AppDatabase({required String encryptionKey}) : super(_openConnection(encryptionKey)); // ========== Categories ========== Future> getAllCategories() { return (select(categories)..where((c) => c.isDeleted.equals(false))).get(); } Future upsertCategory(CategoriesCompanion category) { return into(categories).insertOnConflictUpdate(category); } Future deleteCategory(String uuid) { return (update(categories)..where((c) => c.uuid.equals(uuid))).write( CategoriesCompanion(isDeleted: Value(true)), ); } // ========== Notes ========== Future> getAllNotes() { return (select(notes) ..orderBy([(note) => OrderingTerm(expression: note.sortIndex)]) ..where((n) => n.isDeleted.equals(false))) .get(); } Future insertNoteAtTop(NotesCompanion note) { return transaction(() async { await customStatement( 'UPDATE notes SET sort_index = sort_index + 1 WHERE is_deleted = 0', ); return into(notes).insert(note.copyWith(sortIndex: const Value(0))); }); } Future replaceAllNotes(List noteList) { return transaction(() async { await (delete(notes)..where((n) => n.isDeleted.equals(false))).go(); for (final NotesCompanion note in noteList) { await into(notes).insert(note); } }); } Future updateNoteRow(DbNote note) { return update(notes).replace(note); } Future deleteNote(int id, int removedIndex) async { await (update(notes)..where((n) => n.id.equals(id))).write( NotesCompanion( isDeleted: const Value(true), updatedAt: Value(DateTime.now()), ), ); await customStatement( 'UPDATE notes SET sort_index = sort_index - 1 WHERE sort_index > ? AND is_deleted = 0', [removedIndex], ); } Future deleteNoteAndShift({ required int id, required int removedIndex, }) { return deleteNote(id, removedIndex); } Future moveNote({ required int id, required int oldIndex, required int newIndex, }) { if (oldIndex == newIndex) { return Future.value(); } return transaction(() async { if (oldIndex < newIndex) { await customStatement( 'UPDATE notes SET sort_index = sort_index - 1 WHERE sort_index > ? AND sort_index <= ? AND is_deleted = 0', [oldIndex, newIndex], ); } else { await customStatement( 'UPDATE notes SET sort_index = sort_index + 1 WHERE sort_index >= ? AND sort_index < ? AND is_deleted = 0', [newIndex, oldIndex], ); } await customStatement( 'UPDATE notes SET sort_index = ?, updated_at = ? WHERE id = ?', [newIndex, DateTime.now().toIso8601String(), id], ); }); } Future> getNotesChangedSince(DateTime since) { return (select( notes, )..where((n) => n.updatedAt.isBiggerThanValue(since))).get(); } Future> getCategoriesChangedSince(DateTime since) { return (select( categories, )..where((c) => c.updatedAt.isBiggerThanValue(since))).get(); } // ========== Sync helpers ========== Future> getUnsyncedNotes() { return (select(notes) ..where((n) => n.isDeleted.equals(true) | n.serverVersion.equals(0))) .get(); } Future> getUnsyncedCategories() { return (select(categories) ..where((c) => c.isDeleted.equals(true) | c.serverVersion.equals(0))) .get(); } } LazyDatabase _openConnection(String encryptionKey) { return LazyDatabase(() async { final Directory supportDir = await getApplicationSupportDirectory(); final File file = File(p.join(supportDir.path, 'notes.sqlite')); return NativeDatabase( file, setup: (database) { final String escapedKey = encryptionKey.replaceAll("'", "''"); // sqlite3mc can emulate SQLCipher file format for compatibility. database.execute("PRAGMA cipher = 'sqlcipher'"); database.execute('PRAGMA legacy = 4'); database.execute("PRAGMA key = '$escapedKey'"); }, ); }); }