mirror of https://github.com/AMT-Cheif/drift.git
Improve examples app, fix many issues with joins
This commit is contained in:
parent
50076102ac
commit
d284aca4f6
|
@ -1,5 +1,8 @@
|
||||||
|
/// Provides utilities around sql keywords, like optional escaping etc.
|
||||||
|
library moor.sqlite_keywords;
|
||||||
|
|
||||||
// https://www.sqlite.org/lang_keywords.html
|
// https://www.sqlite.org/lang_keywords.html
|
||||||
const sqliteKeywords = [
|
const sqliteKeywords = {
|
||||||
'ABORT',
|
'ABORT',
|
||||||
'ACTION',
|
'ACTION',
|
||||||
'ADD',
|
'ADD',
|
||||||
|
@ -136,7 +139,7 @@ const sqliteKeywords = [
|
||||||
'WINDOW',
|
'WINDOW',
|
||||||
'WITH',
|
'WITH',
|
||||||
'WITHOUT'
|
'WITHOUT'
|
||||||
];
|
};
|
||||||
|
|
||||||
bool isSqliteKeyword(String s) => sqliteKeywords.contains(s.toUpperCase());
|
bool isSqliteKeyword(String s) => sqliteKeywords.contains(s.toUpperCase());
|
||||||
|
|
|
@ -22,7 +22,10 @@ class InsertStatement<DataClass> {
|
||||||
/// thrown. An insert will also fail if another row with the same primary key
|
/// thrown. An insert will also fail if another row with the same primary key
|
||||||
/// or unique constraints already exists. If you want to override data in that
|
/// or unique constraints already exists. If you want to override data in that
|
||||||
/// case, use [insertOrReplace] instead.
|
/// case, use [insertOrReplace] instead.
|
||||||
Future<void> insert(DataClass entity) async {
|
///
|
||||||
|
/// If the table contains an auto-increment column, the generated value will
|
||||||
|
/// be returned.
|
||||||
|
Future<int> insert(DataClass entity) async {
|
||||||
if (!table.validateIntegrity(entity, true)) {
|
if (!table.validateIntegrity(entity, true)) {
|
||||||
throw InvalidDataException(
|
throw InvalidDataException(
|
||||||
'Invalid data: $entity cannot be written into ${table.$tableName}');
|
'Invalid data: $entity cannot be written into ${table.$tableName}');
|
||||||
|
@ -54,9 +57,10 @@ class InsertStatement<DataClass> {
|
||||||
|
|
||||||
ctx.buffer.write(')');
|
ctx.buffer.write(')');
|
||||||
|
|
||||||
await database.executor.doWhenOpened((e) async {
|
return await database.executor.doWhenOpened((e) async {
|
||||||
await database.executor.runInsert(ctx.sql, ctx.boundVariables);
|
final id = await database.executor.runInsert(ctx.sql, ctx.boundVariables);
|
||||||
database.markTablesUpdated({table});
|
database.markTablesUpdated({table});
|
||||||
|
return id;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,10 @@ import 'package:meta/meta.dart';
|
||||||
import 'package:moor/moor.dart';
|
import 'package:moor/moor.dart';
|
||||||
import 'package:moor/src/runtime/components/component.dart';
|
import 'package:moor/src/runtime/components/component.dart';
|
||||||
import 'package:moor/src/runtime/components/join.dart';
|
import 'package:moor/src/runtime/components/join.dart';
|
||||||
|
import 'package:moor/src/runtime/components/where.dart';
|
||||||
import 'package:moor/src/runtime/database.dart';
|
import 'package:moor/src/runtime/database.dart';
|
||||||
import 'package:moor/src/runtime/executor/stream_queries.dart';
|
import 'package:moor/src/runtime/executor/stream_queries.dart';
|
||||||
|
import 'package:moor/src/runtime/expressions/expression.dart';
|
||||||
import 'package:moor/src/runtime/statements/query.dart';
|
import 'package:moor/src/runtime/statements/query.dart';
|
||||||
import 'package:moor/src/runtime/structure/table_info.dart';
|
import 'package:moor/src/runtime/structure/table_info.dart';
|
||||||
|
|
||||||
|
@ -38,7 +40,17 @@ class JoinedSelectStatement<FirstT, FirstD> extends Query<FirstT, FirstD>
|
||||||
ctx.buffer.write(', ');
|
ctx.buffer.write(', ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We run into problems when two tables have a column with the same name
|
||||||
|
// as we then wouldn't know which column is which. So, we create a
|
||||||
|
// column alias that matches what is expected by the mapping function
|
||||||
|
// in _getWithQuery by prefixing the table name.
|
||||||
|
// We might switch to parsing via the index of the column in a row in
|
||||||
|
// the future, but that's the solution for now.
|
||||||
|
|
||||||
column.writeInto(ctx);
|
column.writeInto(ctx);
|
||||||
|
ctx.buffer.write(' AS "');
|
||||||
|
column.writeInto(ctx, ignoreEscape: true);
|
||||||
|
ctx.buffer.write('"');
|
||||||
|
|
||||||
isFirst = false;
|
isFirst = false;
|
||||||
}
|
}
|
||||||
|
@ -57,9 +69,36 @@ class JoinedSelectStatement<FirstT, FirstD> extends Query<FirstT, FirstD>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void where(Expression<bool, BoolType> predicate) {
|
||||||
|
if (whereExpr == null) {
|
||||||
|
whereExpr = Where(predicate);
|
||||||
|
} else {
|
||||||
|
whereExpr = Where(and(whereExpr.predicate, predicate));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void orderBy(List<OrderingTerm> terms) {
|
||||||
|
orderByExpr = OrderBy(terms);
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<List<TypedResult>> watch() {
|
||||||
|
final ctx = constructQuery();
|
||||||
|
final fetcher = QueryStreamFetcher<List<TypedResult>>(
|
||||||
|
readsFrom: watchedTables,
|
||||||
|
fetchData: () => _getWithQuery(ctx),
|
||||||
|
key: StreamKey(ctx.sql, ctx.boundVariables, TypedResult),
|
||||||
|
);
|
||||||
|
|
||||||
|
return database.createStream(fetcher);
|
||||||
|
}
|
||||||
|
|
||||||
/// Executes this statement and returns the result.
|
/// Executes this statement and returns the result.
|
||||||
Future<List<TypedResult>> get() async {
|
Future<List<TypedResult>> get() async {
|
||||||
final ctx = constructQuery();
|
final ctx = constructQuery();
|
||||||
|
return _getWithQuery(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<TypedResult>> _getWithQuery(GenerationContext ctx) async {
|
||||||
final results = await ctx.database.executor.doWhenOpened((e) async {
|
final results = await ctx.database.executor.doWhenOpened((e) async {
|
||||||
return await e.runSelect(ctx.sql, ctx.boundVariables);
|
return await e.runSelect(ctx.sql, ctx.boundVariables);
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,12 +7,15 @@ import 'package:moor/src/runtime/expressions/expression.dart';
|
||||||
import 'package:moor/src/runtime/expressions/text.dart';
|
import 'package:moor/src/runtime/expressions/text.dart';
|
||||||
import 'package:moor/src/runtime/expressions/variables.dart';
|
import 'package:moor/src/runtime/expressions/variables.dart';
|
||||||
import 'package:moor/src/types/sql_types.dart';
|
import 'package:moor/src/types/sql_types.dart';
|
||||||
|
import 'package:moor/sqlite_keywords.dart';
|
||||||
|
|
||||||
/// Base class for the implementation of [Column].
|
/// Base class for the implementation of [Column].
|
||||||
abstract class GeneratedColumn<T, S extends SqlType<T>> extends Column<T, S> {
|
abstract class GeneratedColumn<T, S extends SqlType<T>> extends Column<T, S> {
|
||||||
/// The sql name of this column.
|
/// The sql name of this column.
|
||||||
final String $name;
|
final String $name;
|
||||||
|
|
||||||
|
String get escapedName => escapeIfNeeded($name);
|
||||||
|
|
||||||
/// The name of the table that contains this column
|
/// The name of the table that contains this column
|
||||||
final String tableName;
|
final String tableName;
|
||||||
|
|
||||||
|
@ -31,7 +34,7 @@ abstract class GeneratedColumn<T, S extends SqlType<T>> extends Column<T, S> {
|
||||||
/// [here](https://www.sqlite.org/syntax/column-def.html), into the given
|
/// [here](https://www.sqlite.org/syntax/column-def.html), into the given
|
||||||
/// buffer.
|
/// buffer.
|
||||||
void writeColumnDefinition(StringBuffer into) {
|
void writeColumnDefinition(StringBuffer into) {
|
||||||
into.write('${$name} $typeName ');
|
into.write('$escapedName $typeName ');
|
||||||
|
|
||||||
if ($customConstraints == null) {
|
if ($customConstraints == null) {
|
||||||
into.write($nullable ? 'NULL' : 'NOT NULL');
|
into.write($nullable ? 'NULL' : 'NOT NULL');
|
||||||
|
@ -50,11 +53,11 @@ abstract class GeneratedColumn<T, S extends SqlType<T>> extends Column<T, S> {
|
||||||
String get typeName;
|
String get typeName;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void writeInto(GenerationContext context) {
|
void writeInto(GenerationContext context, {bool ignoreEscape = false}) {
|
||||||
if (context.hasMultipleTables) {
|
if (context.hasMultipleTables) {
|
||||||
context.buffer..write(tableName)..write('.');
|
context.buffer..write(tableName)..write('.');
|
||||||
}
|
}
|
||||||
context.buffer.write($name);
|
context.buffer.write(ignoreEscape ? $name : escapedName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks whether the given value fits into this column. The default
|
/// Checks whether the given value fits into this column. The default
|
||||||
|
|
|
@ -221,7 +221,7 @@ class Category {
|
||||||
return Category(
|
return Category(
|
||||||
id: intType.mapFromDatabaseResponse(data['${effectivePrefix}id']),
|
id: intType.mapFromDatabaseResponse(data['${effectivePrefix}id']),
|
||||||
description:
|
description:
|
||||||
stringType.mapFromDatabaseResponse(data['${effectivePrefix}`desc`']),
|
stringType.mapFromDatabaseResponse(data['${effectivePrefix}desc']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
factory Category.fromJson(Map<String, dynamic> json) {
|
factory Category.fromJson(Map<String, dynamic> json) {
|
||||||
|
@ -277,9 +277,9 @@ class $CategoriesTable extends Categories
|
||||||
GeneratedTextColumn get description =>
|
GeneratedTextColumn get description =>
|
||||||
_description ??= _constructDescription();
|
_description ??= _constructDescription();
|
||||||
GeneratedTextColumn _constructDescription() {
|
GeneratedTextColumn _constructDescription() {
|
||||||
var cName = '`desc`';
|
var cName = 'desc';
|
||||||
if (_alias != null) cName = '$_alias.$cName';
|
if (_alias != null) cName = '$_alias.$cName';
|
||||||
return GeneratedTextColumn('`desc`', $tableName, false,
|
return GeneratedTextColumn('desc', $tableName, false,
|
||||||
$customConstraints: 'NOT NULL UNIQUE');
|
$customConstraints: 'NOT NULL UNIQUE');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -310,7 +310,7 @@ class $CategoriesTable extends Categories
|
||||||
map['id'] = Variable<int, IntType>(d.id);
|
map['id'] = Variable<int, IntType>(d.id);
|
||||||
}
|
}
|
||||||
if (d.description != null || includeNulls) {
|
if (d.description != null || includeNulls) {
|
||||||
map['`desc`'] = Variable<String, StringType>(d.description);
|
map['desc'] = Variable<String, StringType>(d.description);
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,14 +9,14 @@ typedef Expression<int, IntType> _Extractor(
|
||||||
/// Tests the top level [year], [month], ..., [second] methods
|
/// Tests the top level [year], [month], ..., [second] methods
|
||||||
void main() {
|
void main() {
|
||||||
final expectedResults = <_Extractor, String>{
|
final expectedResults = <_Extractor, String>{
|
||||||
year: 'CAST(strftime("%Y", column, "unixepoch") AS INTEGER)',
|
year: 'CAST(strftime("%Y", val, "unixepoch") AS INTEGER)',
|
||||||
month: 'CAST(strftime("%m", column, "unixepoch") AS INTEGER)',
|
month: 'CAST(strftime("%m", val, "unixepoch") AS INTEGER)',
|
||||||
day: 'CAST(strftime("%d", column, "unixepoch") AS INTEGER)',
|
day: 'CAST(strftime("%d", val, "unixepoch") AS INTEGER)',
|
||||||
hour: 'CAST(strftime("%H", column, "unixepoch") AS INTEGER)',
|
hour: 'CAST(strftime("%H", val, "unixepoch") AS INTEGER)',
|
||||||
minute: 'CAST(strftime("%M", column, "unixepoch") AS INTEGER)',
|
minute: 'CAST(strftime("%M", val, "unixepoch") AS INTEGER)',
|
||||||
second: 'CAST(strftime("%S", column, "unixepoch") AS INTEGER)',
|
second: 'CAST(strftime("%S", val, "unixepoch") AS INTEGER)',
|
||||||
};
|
};
|
||||||
final column = GeneratedDateTimeColumn('column', null, false);
|
final column = GeneratedDateTimeColumn('val', null, false);
|
||||||
|
|
||||||
expectedResults.forEach((key, value) {
|
expectedResults.forEach((key, value) {
|
||||||
test('should extract field', () {
|
test('should extract field', () {
|
||||||
|
|
|
@ -55,4 +55,11 @@ void main() {
|
||||||
throwsA(const TypeMatcher<InvalidDataException>()),
|
throwsA(const TypeMatcher<InvalidDataException>()),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('reports auto-increment id', () async {
|
||||||
|
when(executor.runInsert(any, any)).thenAnswer((_) => Future.value(42));
|
||||||
|
|
||||||
|
expect(db.into(db.todosTable).insert(TodoEntry(content: 'Bottom text')),
|
||||||
|
completion(42));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,19 +13,6 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('generates join statements', () async {
|
test('generates join statements', () async {
|
||||||
await db.select(db.todosTable).join([
|
|
||||||
leftOuterJoin(
|
|
||||||
db.categories, db.categories.id.equalsExp(db.todosTable.category))
|
|
||||||
]).get();
|
|
||||||
|
|
||||||
verify(executor.runSelect(
|
|
||||||
'SELECT todos.id, todos.title, todos.content, todos.target_date, '
|
|
||||||
'todos.category, categories.id, categories.`desc` FROM todos '
|
|
||||||
'LEFT OUTER JOIN categories ON categories.id = todos.category;',
|
|
||||||
argThat(isEmpty)));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('generates join statements with table aliases', () async {
|
|
||||||
final todos = db.alias(db.todosTable, 't');
|
final todos = db.alias(db.todosTable, 't');
|
||||||
final categories = db.alias(db.categories, 'c');
|
final categories = db.alias(db.categories, 'c');
|
||||||
|
|
||||||
|
@ -34,9 +21,10 @@ void main() {
|
||||||
]).get();
|
]).get();
|
||||||
|
|
||||||
verify(executor.runSelect(
|
verify(executor.runSelect(
|
||||||
'SELECT t.id, t.title, t.content, t.target_date, '
|
'SELECT t.id AS "t.id", t.title AS "t.title", t.content AS "t.content", '
|
||||||
't.category, c.id, c.`desc` FROM todos t '
|
't.target_date AS "t.target_date", '
|
||||||
'LEFT OUTER JOIN categories c ON c.id = t.category;',
|
't.category AS "t.category", c.id AS "c.id", c.`desc` AS "c.desc" '
|
||||||
|
'FROM todos t LEFT OUTER JOIN categories c ON c.id = t.category;',
|
||||||
argThat(isEmpty)));
|
argThat(isEmpty)));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -54,7 +42,7 @@ void main() {
|
||||||
't.target_date': date.millisecondsSinceEpoch ~/ 1000,
|
't.target_date': date.millisecondsSinceEpoch ~/ 1000,
|
||||||
't.category': 3,
|
't.category': 3,
|
||||||
'c.id': 3,
|
'c.id': 3,
|
||||||
'c.`desc`': 'description',
|
'c.desc': 'description',
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,14 +1,86 @@
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:moor_example/database/database.dart';
|
import 'package:moor_example/database/database.dart';
|
||||||
|
import 'package:rxdart/rxdart.dart';
|
||||||
|
|
||||||
|
/// Class that keeps information about a category and whether it's selected at
|
||||||
|
/// the moment.
|
||||||
|
class CategoryWithActiveInfo {
|
||||||
|
CategoryWithCount categoryWithCount;
|
||||||
|
bool isActive;
|
||||||
|
|
||||||
|
CategoryWithActiveInfo(this.categoryWithCount, this.isActive);
|
||||||
|
}
|
||||||
|
|
||||||
class TodoAppBloc {
|
class TodoAppBloc {
|
||||||
final Database db;
|
final Database db;
|
||||||
Stream<List<TodoEntry>> get allEntries => db.allEntries();
|
|
||||||
|
|
||||||
TodoAppBloc() : db = Database();
|
// the category that is selected at the moment. null means that we show all
|
||||||
|
// entries
|
||||||
|
final BehaviorSubject<Category> _activeCategory =
|
||||||
|
BehaviorSubject.seeded(null);
|
||||||
|
|
||||||
void addEntry(TodoEntry entry) {
|
Observable<List<EntryWithCategory>> _currentEntries;
|
||||||
db.addEntry(entry);
|
|
||||||
|
/// A stream of entries that should be displayed on the home screen.
|
||||||
|
Observable<List<EntryWithCategory>> get homeScreenEntries => _currentEntries;
|
||||||
|
|
||||||
|
final BehaviorSubject<List<CategoryWithActiveInfo>> _allCategories =
|
||||||
|
BehaviorSubject();
|
||||||
|
Observable<List<CategoryWithActiveInfo>> get categories => _allCategories;
|
||||||
|
|
||||||
|
TodoAppBloc() : db = Database() {
|
||||||
|
// listen for the category to change. Then display all entries that are in
|
||||||
|
// the current category on the home screen.
|
||||||
|
_currentEntries = _activeCategory.switchMap(db.watchEntriesInCategory);
|
||||||
|
|
||||||
|
// also watch all categories so that they can be displayed in the navigation
|
||||||
|
// drawer.
|
||||||
|
Observable.combineLatest2<List<CategoryWithCount>, Category,
|
||||||
|
List<CategoryWithActiveInfo>>(
|
||||||
|
db.categoriesWithCount(),
|
||||||
|
_activeCategory,
|
||||||
|
(allCategories, selected) {
|
||||||
|
return allCategories.map((category) {
|
||||||
|
final isActive = selected?.id == category.category?.id;
|
||||||
|
|
||||||
|
return CategoryWithActiveInfo(category, isActive);
|
||||||
|
}).toList();
|
||||||
|
},
|
||||||
|
).listen(_allCategories.add);
|
||||||
|
}
|
||||||
|
|
||||||
|
void showCategory(Category category) {
|
||||||
|
_activeCategory.add(category);
|
||||||
|
}
|
||||||
|
|
||||||
|
void addCategory(String description) async {
|
||||||
|
final category = Category(description: description);
|
||||||
|
final id = await db.createCategory(category);
|
||||||
|
|
||||||
|
showCategory(category.copyWith(id: id));
|
||||||
|
}
|
||||||
|
|
||||||
|
void createEntry(String content) {
|
||||||
|
db.createEntry(TodoEntry(
|
||||||
|
content: content,
|
||||||
|
category: _activeCategory.value?.id,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateEntry(TodoEntry entry) {
|
||||||
|
db.updateEntry(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
void deleteEntry(TodoEntry entry) {
|
||||||
|
db.deleteEntry(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
void deleteCategory(Category category) {
|
||||||
|
// if the category being deleted is the one selected, reset that state by
|
||||||
|
// showing the entries who aren't in any category
|
||||||
|
if (_activeCategory.value?.id == category.id) {
|
||||||
|
showCategory(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
db.deleteCategory(category);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:moor_example/database/todos_dao.dart';
|
|
||||||
import 'package:moor_flutter/moor_flutter.dart';
|
import 'package:moor_flutter/moor_flutter.dart';
|
||||||
|
|
||||||
part 'database.g.dart';
|
part 'database.g.dart';
|
||||||
|
@ -25,6 +24,7 @@ class Categories extends Table {
|
||||||
class CategoryWithCount {
|
class CategoryWithCount {
|
||||||
CategoryWithCount(this.category, this.count);
|
CategoryWithCount(this.category, this.count);
|
||||||
|
|
||||||
|
// can be null, in which case we count how many entries don't have a category
|
||||||
final Category category;
|
final Category category;
|
||||||
final int count; // amount of entries in this category
|
final int count; // amount of entries in this category
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@ class EntryWithCategory {
|
||||||
final Category category;
|
final Category category;
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseMoor(tables: [Todos, Categories], daos: [TodosDao])
|
@UseMoor(tables: [Todos, Categories])
|
||||||
class Database extends _$Database {
|
class Database extends _$Database {
|
||||||
Database()
|
Database()
|
||||||
: super(FlutterQueryExecutor.inDatabaseFolder(
|
: super(FlutterQueryExecutor.inDatabaseFolder(
|
||||||
|
@ -46,73 +46,91 @@ class Database extends _$Database {
|
||||||
int get schemaVersion => 1;
|
int get schemaVersion => 1;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration => MigrationStrategy(onCreate: (Migrator m) {
|
MigrationStrategy get migration {
|
||||||
|
return MigrationStrategy(
|
||||||
|
onCreate: (Migrator m) {
|
||||||
return m.createAllTables();
|
return m.createAllTables();
|
||||||
}, onUpgrade: (Migrator m, int from, int to) async {
|
},
|
||||||
|
onUpgrade: (Migrator m, int from, int to) async {
|
||||||
if (from == 1) {
|
if (from == 1) {
|
||||||
await m.addColumn(todos, todos.targetDate);
|
await m.addColumn(todos, todos.targetDate);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Stream<List<CategoryWithCount>> categoriesWithCount() {
|
Stream<List<CategoryWithCount>> categoriesWithCount() {
|
||||||
// select all categories and load how many associated entries there are for
|
// select all categories and load how many associated entries there are for
|
||||||
// each category
|
// each category
|
||||||
return customSelectStream(
|
return customSelectStream(
|
||||||
'SELECT *, (SELECT COUNT(*) FROM todos WHERE category = c.id) AS "amount" FROM categories c;',
|
'SELECT c.*, (SELECT COUNT(*) FROM todos WHERE category = c.id) AS amount'
|
||||||
readsFrom: {todos, categories})
|
' FROM categories c '
|
||||||
.map((rows) {
|
'UNION ALL SELECT null, null, '
|
||||||
|
'(SELECT COUNT(*) FROM todos WHERE category IS NULL)',
|
||||||
|
readsFrom: {todos, categories},
|
||||||
|
).map((rows) {
|
||||||
// when we have the result set, map each row to the data class
|
// when we have the result set, map each row to the data class
|
||||||
return rows
|
return rows.map((row) {
|
||||||
.map((row) => CategoryWithCount(
|
final hasId = row.data['id'] != null;
|
||||||
Category.fromData(row.data, this), row.readInt('amount')))
|
|
||||||
.toList();
|
return CategoryWithCount(
|
||||||
|
hasId ? Category.fromData(row.data, this) : null,
|
||||||
|
row.readInt('amount'),
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<EntryWithCategory>> entriesWithCategories() async {
|
/// Watches all entries in the given [category]. If the category is null, all
|
||||||
final results = await select(todos).join([
|
/// entries will be shown instead.
|
||||||
leftOuterJoin(categories, categories.id.equalsExp(todos.category))
|
Stream<List<EntryWithCategory>> watchEntriesInCategory(Category category) {
|
||||||
]).get();
|
final query = select(todos).join(
|
||||||
|
[leftOuterJoin(categories, categories.id.equalsExp(todos.category))]);
|
||||||
|
|
||||||
return results.map((row) {
|
if (category != null) {
|
||||||
return EntryWithCategory(row.readTable(todos), row.readTable(categories));
|
query.where(categories.id.equals(category.id));
|
||||||
}).toList();
|
} else {
|
||||||
|
query.where(isNull(categories.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.watch().map((rows) {
|
||||||
|
// read both the entry and the associated category for each row
|
||||||
|
return rows.map((row) {
|
||||||
|
return EntryWithCategory(
|
||||||
|
row.readTable(todos),
|
||||||
|
row.readTable(categories),
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<List<TodoEntry>> allEntries() {
|
Future createEntry(TodoEntry entry) {
|
||||||
return select(todos).watch();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future addEntry(TodoEntry entry) {
|
|
||||||
return into(todos).insert(entry);
|
return into(todos).insert(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Updates the row in the database represents this entry by writing the
|
||||||
|
/// updated data.
|
||||||
|
Future updateEntry(TodoEntry entry) {
|
||||||
|
return update(todos).replace(entry);
|
||||||
|
}
|
||||||
|
|
||||||
Future deleteEntry(TodoEntry entry) {
|
Future deleteEntry(TodoEntry entry) {
|
||||||
return delete(todos).delete(entry);
|
return delete(todos).delete(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future updateContent(int id, String content) {
|
Future<int> createCategory(Category category) {
|
||||||
return (update(todos)..where((t) => t.id.equals(id)))
|
return into(categories).insert(category);
|
||||||
.write(TodoEntry(content: content));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future updateDate(int id, DateTime dueDate) {
|
Future deleteCategory(Category category) {
|
||||||
return (update(todos)..where((t) => t.id.equals(id)))
|
return transaction((t) async {
|
||||||
.write(TodoEntry(targetDate: dueDate));
|
await t.customUpdate(
|
||||||
}
|
'UPDATE todos SET category = NULL WHERE category = ?',
|
||||||
|
updates: {todos},
|
||||||
Future testTransaction(TodoEntry entry) {
|
variables: [Variable.withInt(category.id)],
|
||||||
return transaction((t) {
|
|
||||||
final updatedContent = entry.copyWith(
|
|
||||||
content: entry.content.toUpperCase(),
|
|
||||||
);
|
|
||||||
t.update(todos).replace(updatedContent);
|
|
||||||
|
|
||||||
final updatedDate = updatedContent.copyWith(
|
|
||||||
targetDate: DateTime.now(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
t.update(todos).replace(updatedDate);
|
await t.delete(categories).delete(category);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -191,7 +191,7 @@ class Category {
|
||||||
return Category(
|
return Category(
|
||||||
id: intType.mapFromDatabaseResponse(data['${effectivePrefix}id']),
|
id: intType.mapFromDatabaseResponse(data['${effectivePrefix}id']),
|
||||||
description:
|
description:
|
||||||
stringType.mapFromDatabaseResponse(data['${effectivePrefix}`desc`']),
|
stringType.mapFromDatabaseResponse(data['${effectivePrefix}desc']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
factory Category.fromJson(Map<String, dynamic> json) {
|
factory Category.fromJson(Map<String, dynamic> json) {
|
||||||
|
@ -247,10 +247,10 @@ class $CategoriesTable extends Categories
|
||||||
GeneratedTextColumn get description =>
|
GeneratedTextColumn get description =>
|
||||||
_description ??= _constructDescription();
|
_description ??= _constructDescription();
|
||||||
GeneratedTextColumn _constructDescription() {
|
GeneratedTextColumn _constructDescription() {
|
||||||
var cName = '`desc`';
|
var cName = 'desc';
|
||||||
if (_alias != null) cName = '$_alias.$cName';
|
if (_alias != null) cName = '$_alias.$cName';
|
||||||
return GeneratedTextColumn(
|
return GeneratedTextColumn(
|
||||||
'`desc`',
|
'desc',
|
||||||
$tableName,
|
$tableName,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
@ -283,7 +283,7 @@ class $CategoriesTable extends Categories
|
||||||
map['id'] = Variable<int, IntType>(d.id);
|
map['id'] = Variable<int, IntType>(d.id);
|
||||||
}
|
}
|
||||||
if (d.description != null || includeNulls) {
|
if (d.description != null || includeNulls) {
|
||||||
map['`desc`'] = Variable<String, StringType>(d.description);
|
map['desc'] = Variable<String, StringType>(d.description);
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:moor/moor.dart';
|
|
||||||
import 'database.dart';
|
|
||||||
|
|
||||||
part 'todos_dao.g.dart';
|
|
||||||
|
|
||||||
@UseDao(tables: [Todos])
|
|
||||||
class TodosDao extends DatabaseAccessor<Database> with _$TodosDaoMixin {
|
|
||||||
TodosDao(Database db) : super(db);
|
|
||||||
|
|
||||||
Stream<List<TodoEntry>> todosInCategory(Category category) {
|
|
||||||
if (category == null) {
|
|
||||||
return (select(todos)..where((t) => isNull(t.category))).watch();
|
|
||||||
} else {
|
|
||||||
return (select(todos)..where((t) => t.category.equals(category.id)))
|
|
||||||
.watch();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'todos_dao.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// DaoGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
mixin _$TodosDaoMixin on DatabaseAccessor<Database> {
|
|
||||||
$TodosTable get todos => db.todos;
|
|
||||||
}
|
|
|
@ -29,7 +29,13 @@ class MyAppState extends State<MyApp> {
|
||||||
child: MaterialApp(
|
child: MaterialApp(
|
||||||
title: 'moor Demo',
|
title: 'moor Demo',
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
primarySwatch: Colors.purple,
|
primarySwatch: Colors.orange,
|
||||||
|
// use the good-looking updated material text style
|
||||||
|
typography: Typography(
|
||||||
|
englishLike: Typography.englishLike2018,
|
||||||
|
dense: Typography.dense2018,
|
||||||
|
tall: Typography.tall2018,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
home: HomeScreen(),
|
home: HomeScreen(),
|
||||||
),
|
),
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:moor_example/main.dart';
|
||||||
|
|
||||||
|
class AddCategoryDialog extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
_AddCategoryDialogState createState() => _AddCategoryDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AddCategoryDialogState extends State<AddCategoryDialog> {
|
||||||
|
final TextEditingController _controller = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Dialog(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Text(
|
||||||
|
'Add a category',
|
||||||
|
style: Theme.of(context).textTheme.title,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextField(
|
||||||
|
controller: _controller,
|
||||||
|
autofocus: true,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Name of the category',
|
||||||
|
),
|
||||||
|
onSubmitted: (_) => _addEntry(),
|
||||||
|
),
|
||||||
|
ButtonBar(
|
||||||
|
children: [
|
||||||
|
FlatButton(
|
||||||
|
child: const Text('Add'),
|
||||||
|
textColor: Theme.of(context).accentColor,
|
||||||
|
onPressed: _addEntry,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _addEntry() {
|
||||||
|
if (_controller.text.isNotEmpty) {
|
||||||
|
BlocProvider.provideBloc(context).addCategory(_controller.text);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,153 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:moor_example/bloc.dart';
|
||||||
|
import 'package:moor_example/main.dart';
|
||||||
|
import 'package:moor_example/widgets/add_category_dialog.dart';
|
||||||
|
|
||||||
|
class CategoriesDrawer extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Drawer(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: <Widget>[
|
||||||
|
DrawerHeader(
|
||||||
|
child: Text(
|
||||||
|
'Todo-List Demo with moor',
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.subhead
|
||||||
|
.copyWith(color: Colors.white),
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(color: Colors.orange),
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: StreamBuilder<List<CategoryWithActiveInfo>>(
|
||||||
|
stream: BlocProvider.provideBloc(context).categories,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final categories = snapshot.data ?? <CategoryWithActiveInfo>[];
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return _CategoryDrawerEntry(entry: categories[index]);
|
||||||
|
},
|
||||||
|
itemCount: categories.length,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Spacer(),
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
FlatButton(
|
||||||
|
child: const Text('Add category'),
|
||||||
|
textColor: Theme.of(context).accentColor,
|
||||||
|
onPressed: () {
|
||||||
|
showDialog(
|
||||||
|
context: context, builder: (_) => AddCategoryDialog());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CategoryDrawerEntry extends StatelessWidget {
|
||||||
|
final CategoryWithActiveInfo entry;
|
||||||
|
|
||||||
|
const _CategoryDrawerEntry({Key key, this.entry}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final category = entry.categoryWithCount.category;
|
||||||
|
String title;
|
||||||
|
if (category == null) {
|
||||||
|
title = 'No category';
|
||||||
|
} else {
|
||||||
|
title = category.description ?? 'Unnamed';
|
||||||
|
}
|
||||||
|
|
||||||
|
final isActive = entry.isActive;
|
||||||
|
final bloc = BlocProvider.provideBloc(context);
|
||||||
|
|
||||||
|
final rowContent = [
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: isActive ? Theme.of(context).accentColor : Colors.black,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Text('${entry.categoryWithCount?.count} entries'),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
// also show a delete button if the category can be deleted
|
||||||
|
if (category != null) {
|
||||||
|
rowContent.addAll([
|
||||||
|
Spacer(),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.delete_outline),
|
||||||
|
color: Colors.red,
|
||||||
|
onPressed: () async {
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Delete'),
|
||||||
|
content: Text('Really delete category $title?'),
|
||||||
|
actions: <Widget>[
|
||||||
|
FlatButton(
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context, false);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
FlatButton(
|
||||||
|
child: const Text('Delete'),
|
||||||
|
textColor: Colors.red,
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context, true);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirmed == true) {
|
||||||
|
// can be null when the dialog is dismissed
|
||||||
|
bloc.deleteCategory(category);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
|
child: Material(
|
||||||
|
color: isActive
|
||||||
|
? Colors.orangeAccent.withOpacity(0.3)
|
||||||
|
: Colors.transparent,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () {
|
||||||
|
bloc.showCategory(entry.categoryWithCount.category);
|
||||||
|
Navigator.pop(context); // close the navigation drawer
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Row(
|
||||||
|
children: rowContent,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ import 'package:flutter/widgets.dart' as f show Column;
|
||||||
import 'package:moor_example/bloc.dart';
|
import 'package:moor_example/bloc.dart';
|
||||||
import 'package:moor_example/database/database.dart';
|
import 'package:moor_example/database/database.dart';
|
||||||
import 'package:moor_example/main.dart';
|
import 'package:moor_example/main.dart';
|
||||||
|
import 'package:moor_example/widgets/categories_drawer.dart';
|
||||||
import 'package:moor_example/widgets/todo_card.dart';
|
import 'package:moor_example/widgets/todo_card.dart';
|
||||||
import 'package:moor_flutter/moor_flutter.dart';
|
import 'package:moor_flutter/moor_flutter.dart';
|
||||||
|
|
||||||
|
@ -28,34 +29,35 @@ class HomeScreenState extends State<HomeScreen> {
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text('Todo list'),
|
title: Text('Todo list'),
|
||||||
),
|
),
|
||||||
|
drawer: CategoriesDrawer(),
|
||||||
// A moorAnimatedList automatically animates incoming and leaving items, we only
|
// A moorAnimatedList automatically animates incoming and leaving items, we only
|
||||||
// have to tell it what data to display and how to turn data into widgets.
|
// have to tell it what data to display and how to turn data into widgets.
|
||||||
body: MoorAnimatedList<TodoEntry>(
|
body: MoorAnimatedList<EntryWithCategory>(
|
||||||
stream: bloc
|
// we want to show an updating stream of all relevant entries
|
||||||
.allEntries, // we want to show an updating stream of all entries
|
stream: bloc.homeScreenEntries,
|
||||||
// consider items equal if their id matches. Otherwise, we'd get an
|
// consider items equal if their id matches. Otherwise, we'd get an
|
||||||
// animation of an old item leaving and another one coming in every time
|
// animation of an old item leaving and another one coming in every time
|
||||||
// the content of an item changed!
|
// the content of an item changed!
|
||||||
equals: (a, b) => a.id == b.id,
|
equals: (a, b) => a.entry.id == b.entry.id,
|
||||||
itemBuilder: (ctx, item, animation) {
|
itemBuilder: (ctx, item, animation) {
|
||||||
// When a new item arrives, it will expand vertically
|
// When a new item arrives, it will expand vertically
|
||||||
return SizeTransition(
|
return SizeTransition(
|
||||||
key: ObjectKey(item.id),
|
key: ObjectKey(item.entry.id),
|
||||||
sizeFactor: animation,
|
sizeFactor: animation,
|
||||||
axis: Axis.vertical,
|
axis: Axis.vertical,
|
||||||
child: TodoCard(item),
|
child: TodoCard(item.entry),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
removedItemBuilder: (ctx, item, animation) {
|
removedItemBuilder: (ctx, item, animation) {
|
||||||
// and it will leave the same way after being deleted.
|
// and it will leave the same way after being deleted.
|
||||||
return SizeTransition(
|
return SizeTransition(
|
||||||
key: ObjectKey(item.id),
|
key: ObjectKey(item.entry.id),
|
||||||
sizeFactor: animation,
|
sizeFactor: animation,
|
||||||
axis: Axis.vertical,
|
axis: Axis.vertical,
|
||||||
child: AnimatedBuilder(
|
child: AnimatedBuilder(
|
||||||
animation:
|
animation:
|
||||||
CurvedAnimation(parent: animation, curve: Curves.easeOut),
|
CurvedAnimation(parent: animation, curve: Curves.easeOut),
|
||||||
child: TodoCard(item),
|
child: TodoCard(item.entry),
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
return Opacity(
|
return Opacity(
|
||||||
opacity: animation.value,
|
opacity: animation.value,
|
||||||
|
@ -102,7 +104,7 @@ class HomeScreenState extends State<HomeScreen> {
|
||||||
if (controller.text.isNotEmpty) {
|
if (controller.text.isNotEmpty) {
|
||||||
// We write the entry here. Notice how we don't have to call setState()
|
// We write the entry here. Notice how we don't have to call setState()
|
||||||
// or anything - moor will take care of updating the list automatically.
|
// or anything - moor will take care of updating the list automatically.
|
||||||
bloc.addEntry(TodoEntry(content: controller.text));
|
bloc.createEntry(controller.text);
|
||||||
controller.clear();
|
controller.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:moor_example/database/database.dart';
|
import 'package:moor_example/database/database.dart';
|
||||||
import 'package:moor_example/main.dart';
|
import 'package:moor_example/main.dart';
|
||||||
import 'package:moor_example/widgets/todo_edit_dialog.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:moor_example/widgets/todo_edit_dialog.dart';
|
||||||
|
|
||||||
final DateFormat _format = DateFormat.yMMMd();
|
final DateFormat _format = DateFormat.yMMMd();
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ class TodoCard extends StatelessWidget {
|
||||||
if (entry.targetDate == null) {
|
if (entry.targetDate == null) {
|
||||||
dueDate = GestureDetector(
|
dueDate = GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
BlocProvider.provideBloc(context).db.testTransaction(entry);
|
// BlocProvider.provideBloc(context).db.testTransaction(entry);
|
||||||
},
|
},
|
||||||
child: const Text(
|
child: const Text(
|
||||||
'No due date set',
|
'No due date set',
|
||||||
|
@ -48,21 +48,6 @@ class TodoCard extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.calendar_today),
|
|
||||||
color: Colors.green,
|
|
||||||
onPressed: () async {
|
|
||||||
final dateTime = await showDatePicker(
|
|
||||||
context: context,
|
|
||||||
initialDate: DateTime.now(),
|
|
||||||
firstDate: DateTime(2019),
|
|
||||||
lastDate: DateTime(3038));
|
|
||||||
|
|
||||||
await BlocProvider.provideBloc(context)
|
|
||||||
.db
|
|
||||||
.updateDate(entry.id, dateTime);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.edit),
|
icon: const Icon(Icons.edit),
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
|
@ -70,9 +55,7 @@ class TodoCard extends StatelessWidget {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
builder: (ctx) => TodoEditDialog(
|
builder: (ctx) => TodoEditDialog(entry: entry),
|
||||||
entry: entry,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -82,7 +65,7 @@ class TodoCard extends StatelessWidget {
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// We delete the entry here. Again, notice how we don't have to call setState() or
|
// We delete the entry here. Again, notice how we don't have to call setState() or
|
||||||
// inform the parent widget. The animated list will take care of this automatically.
|
// inform the parent widget. The animated list will take care of this automatically.
|
||||||
BlocProvider.provideBloc(context).db.deleteEntry(entry);
|
BlocProvider.provideBloc(context).deleteEntry(entry);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:moor_example/database/database.dart';
|
import 'package:moor_example/database/database.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
import 'package:moor_example/main.dart';
|
import 'package:moor_example/main.dart';
|
||||||
|
|
||||||
|
final _dateFormat = DateFormat.yMMMd();
|
||||||
|
|
||||||
class TodoEditDialog extends StatefulWidget {
|
class TodoEditDialog extends StatefulWidget {
|
||||||
final TodoEntry entry;
|
final TodoEntry entry;
|
||||||
|
|
||||||
|
@ -13,10 +16,12 @@ class TodoEditDialog extends StatefulWidget {
|
||||||
|
|
||||||
class _TodoEditDialogState extends State<TodoEditDialog> {
|
class _TodoEditDialogState extends State<TodoEditDialog> {
|
||||||
final TextEditingController textController = TextEditingController();
|
final TextEditingController textController = TextEditingController();
|
||||||
|
DateTime _dueDate;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
textController.text = widget.entry.content;
|
textController.text = widget.entry.content;
|
||||||
|
_dueDate = widget.entry.targetDate;
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,6 +33,11 @@ class _TodoEditDialogState extends State<TodoEditDialog> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
var formattedDate = 'No date set';
|
||||||
|
if (_dueDate != null) {
|
||||||
|
formattedDate = _dateFormat.format(_dueDate);
|
||||||
|
}
|
||||||
|
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: const Text('Edit entry'),
|
title: const Text('Edit entry'),
|
||||||
content: Column(
|
content: Column(
|
||||||
|
@ -40,12 +50,38 @@ class _TodoEditDialogState extends State<TodoEditDialog> {
|
||||||
helperText: 'Content of entry',
|
helperText: 'Content of entry',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Text(formattedDate),
|
||||||
|
Spacer(),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.calendar_today),
|
||||||
|
onPressed: () async {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final initialDate = _dueDate ?? now;
|
||||||
|
final firstDate =
|
||||||
|
initialDate.isBefore(now) ? initialDate : now;
|
||||||
|
|
||||||
|
final selectedDate = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: initialDate,
|
||||||
|
firstDate: firstDate,
|
||||||
|
lastDate: DateTime(3000),
|
||||||
|
);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
if (selectedDate != null) _dueDate = selectedDate;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
FlatButton(
|
FlatButton(
|
||||||
child: const Text('Cancel'),
|
child: const Text('Cancel'),
|
||||||
textColor: Colors.red,
|
textColor: Colors.black,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
|
@ -53,13 +89,13 @@ class _TodoEditDialogState extends State<TodoEditDialog> {
|
||||||
FlatButton(
|
FlatButton(
|
||||||
child: const Text('Save'),
|
child: const Text('Save'),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final entry = widget.entry;
|
final updatedContent = textController.text;
|
||||||
if (textController.text.isNotEmpty) {
|
final entry = widget.entry.copyWith(
|
||||||
BlocProvider.provideBloc(context)
|
content: updatedContent.isNotEmpty ? updatedContent : null,
|
||||||
.db
|
targetDate: _dueDate,
|
||||||
.updateContent(entry.id, textController.text);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
|
BlocProvider.provideBloc(context).updateEntry(entry);
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
@ -11,7 +11,7 @@ dependencies:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
intl:
|
intl:
|
||||||
cupertino_icons: ^0.1.2
|
cupertino_icons: ^0.1.2
|
||||||
rxdart: 0.20.0
|
rxdart: 0.21.0
|
||||||
moor_flutter: ^1.1.0
|
moor_flutter: ^1.1.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import 'package:built_value/built_value.dart';
|
import 'package:built_value/built_value.dart';
|
||||||
import 'package:moor_generator/src/sqlite_keywords.dart' show isSqliteKeyword;
|
|
||||||
|
|
||||||
part 'specified_column.g.dart';
|
part 'specified_column.g.dart';
|
||||||
|
|
||||||
|
@ -18,14 +17,6 @@ abstract class ColumnName implements Built<ColumnName, ColumnNameBuilder> {
|
||||||
|
|
||||||
ColumnName._();
|
ColumnName._();
|
||||||
|
|
||||||
ColumnName escapeIfSqlKeyword() {
|
|
||||||
if (isSqliteKeyword(name)) {
|
|
||||||
return rebuild((b) => b.name = '`$name`'); // wrap name in backticks
|
|
||||||
} else {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
factory ColumnName([updates(ColumnNameBuilder b)]) = _$ColumnName;
|
factory ColumnName([updates(ColumnNameBuilder b)]) = _$ColumnName;
|
||||||
|
|
||||||
factory ColumnName.implicitly(String name) => ColumnName((b) => b
|
factory ColumnName.implicitly(String name) => ColumnName((b) => b
|
||||||
|
|
|
@ -150,7 +150,7 @@ class ColumnParser extends ParserBase {
|
||||||
return SpecifiedColumn(
|
return SpecifiedColumn(
|
||||||
type: _startMethodToColumnType(foundStartMethod),
|
type: _startMethodToColumnType(foundStartMethod),
|
||||||
dartGetterName: getter.name.name,
|
dartGetterName: getter.name.name,
|
||||||
name: name.escapeIfSqlKeyword(),
|
name: name,
|
||||||
declaredAsPrimaryKey: wasDeclaredAsPrimaryKey,
|
declaredAsPrimaryKey: wasDeclaredAsPrimaryKey,
|
||||||
customConstraints: foundCustomConstraint,
|
customConstraints: foundCustomConstraint,
|
||||||
nullable: nullable,
|
nullable: nullable,
|
||||||
|
|
|
@ -4,11 +4,11 @@ import 'package:moor_generator/src/errors.dart';
|
||||||
import 'package:moor_generator/src/model/specified_column.dart';
|
import 'package:moor_generator/src/model/specified_column.dart';
|
||||||
import 'package:moor_generator/src/model/specified_table.dart';
|
import 'package:moor_generator/src/model/specified_table.dart';
|
||||||
import 'package:moor_generator/src/parser/parser.dart';
|
import 'package:moor_generator/src/parser/parser.dart';
|
||||||
import 'package:moor_generator/src/sqlite_keywords.dart';
|
|
||||||
import 'package:moor_generator/src/utils/names.dart';
|
import 'package:moor_generator/src/utils/names.dart';
|
||||||
import 'package:moor_generator/src/utils/type_utils.dart';
|
import 'package:moor_generator/src/utils/type_utils.dart';
|
||||||
import 'package:moor_generator/src/moor_generator.dart'; // ignore: implementation_imports
|
import 'package:moor_generator/src/moor_generator.dart'; // ignore: implementation_imports
|
||||||
import 'package:recase/recase.dart';
|
import 'package:recase/recase.dart';
|
||||||
|
import 'package:moor/sqlite_keywords.dart';
|
||||||
|
|
||||||
class TableParser extends ParserBase {
|
class TableParser extends ParserBase {
|
||||||
TableParser(MoorGenerator generator) : super(generator);
|
TableParser(MoorGenerator generator) : super(generator);
|
||||||
|
|
Loading…
Reference in New Issue