diff --git a/moor/example_web/build.yaml b/moor/example_web/build.yaml new file mode 100644 index 00000000..068564f5 --- /dev/null +++ b/moor/example_web/build.yaml @@ -0,0 +1,11 @@ +# See https://github.com/dart-lang/build/tree/master/build_web_compilers#configuration +targets: + $default: + builders: + build_web_compilers|entrypoint: + generate_for: + - web/**.dart + options: + dart2js_args: + - --no-source-maps + - -O4 \ No newline at end of file diff --git a/moor/example_web/lib/database/database.dart b/moor/example_web/lib/database/database.dart new file mode 100644 index 00000000..4e4d8107 --- /dev/null +++ b/moor/example_web/lib/database/database.dart @@ -0,0 +1,47 @@ +import 'package:moor/moor_web.dart'; + +part 'database.g.dart'; + +const int _doneEntriesCount = 20; + +@DataClassName('Entry') +class TodoEntries extends Table { + IntColumn get id => integer().autoIncrement()(); + + TextColumn get content => text()(); + BoolColumn get done => boolean().withDefault(const Constant(false))(); +} + +@UseMoor(tables: [ + TodoEntries +], queries: { + 'hiddenEntryCount': 'SELECT COUNT(*) - $_doneEntriesCount AS entries ' + 'FROM todo_entries WHERE done' +}) +class Database extends _$Database { + Database() : super(WebDatabase('app', logStatements: true)); + + @override + final int schemaVersion = 1; + + Stream> incompleteEntries() { + return (select(todoEntries)..where((e) => not(e.done))).watch(); + } + + Stream> newestDoneEntries() { + return (select(todoEntries) + ..where((e) => e.done) + ..orderBy( + [(e) => OrderingTerm(expression: e.id, mode: OrderingMode.desc)]) + ..limit(_doneEntriesCount)) + .watch(); + } + + Future createTodoEntry(String desc) { + return into(todoEntries).insert(TodoEntriesCompanion(content: Value(desc))); + } + + Future setCompleted(Entry entry, bool done) { + return update(todoEntries).write(entry.copyWith(done: done)); + } +} diff --git a/moor/example_web/web/database.g.dart b/moor/example_web/lib/database/database.g.dart similarity index 60% rename from moor/example_web/web/database.g.dart rename to moor/example_web/lib/database/database.g.dart index 6060ca21..144573d1 100644 --- a/moor/example_web/web/database.g.dart +++ b/moor/example_web/lib/database/database.g.dart @@ -7,32 +7,30 @@ part of 'database.dart'; // ************************************************************************** // ignore_for_file: unnecessary_brace_in_string_interps -class TodoEntry extends DataClass implements Insertable { +class Entry extends DataClass implements Insertable { final int id; final String content; - final DateTime creationDate; - TodoEntry( - {@required this.id, @required this.content, @required this.creationDate}); - factory TodoEntry.fromData(Map data, GeneratedDatabase db, + final bool done; + Entry({@required this.id, @required this.content, @required this.done}); + factory Entry.fromData(Map data, GeneratedDatabase db, {String prefix}) { final effectivePrefix = prefix ?? ''; final intType = db.typeSystem.forDartType(); final stringType = db.typeSystem.forDartType(); - final dateTimeType = db.typeSystem.forDartType(); - return TodoEntry( + final boolType = db.typeSystem.forDartType(); + return Entry( id: intType.mapFromDatabaseResponse(data['${effectivePrefix}id']), content: stringType.mapFromDatabaseResponse(data['${effectivePrefix}content']), - creationDate: dateTimeType - .mapFromDatabaseResponse(data['${effectivePrefix}creation_date']), + done: boolType.mapFromDatabaseResponse(data['${effectivePrefix}done']), ); } - factory TodoEntry.fromJson(Map json, + factory Entry.fromJson(Map json, {ValueSerializer serializer = const ValueSerializer.defaults()}) { - return TodoEntry( + return Entry( id: serializer.fromJson(json['id']), content: serializer.fromJson(json['content']), - creationDate: serializer.fromJson(json['creationDate']), + done: serializer.fromJson(json['done']), ); } @override @@ -41,64 +39,61 @@ class TodoEntry extends DataClass implements Insertable { return { 'id': serializer.toJson(id), 'content': serializer.toJson(content), - 'creationDate': serializer.toJson(creationDate), + 'done': serializer.toJson(done), }; } @override - T createCompanion>(bool nullToAbsent) { + T createCompanion>(bool nullToAbsent) { return TodoEntriesCompanion( id: id == null && nullToAbsent ? const Value.absent() : Value(id), content: content == null && nullToAbsent ? const Value.absent() : Value(content), - creationDate: creationDate == null && nullToAbsent - ? const Value.absent() - : Value(creationDate), + done: done == null && nullToAbsent ? const Value.absent() : Value(done), ) as T; } - TodoEntry copyWith({int id, String content, DateTime creationDate}) => - TodoEntry( + Entry copyWith({int id, String content, bool done}) => Entry( id: id ?? this.id, content: content ?? this.content, - creationDate: creationDate ?? this.creationDate, + done: done ?? this.done, ); @override String toString() { - return (StringBuffer('TodoEntry(') + return (StringBuffer('Entry(') ..write('id: $id, ') ..write('content: $content, ') - ..write('creationDate: $creationDate') + ..write('done: $done') ..write(')')) .toString(); } @override - int get hashCode => $mrjf($mrjc( - $mrjc($mrjc(0, id.hashCode), content.hashCode), creationDate.hashCode)); + int get hashCode => $mrjf( + $mrjc($mrjc($mrjc(0, id.hashCode), content.hashCode), done.hashCode)); @override bool operator ==(other) => identical(this, other) || - (other is TodoEntry && + (other is Entry && other.id == id && other.content == content && - other.creationDate == creationDate); + other.done == done); } -class TodoEntriesCompanion extends UpdateCompanion { +class TodoEntriesCompanion extends UpdateCompanion { final Value id; final Value content; - final Value creationDate; + final Value done; const TodoEntriesCompanion({ this.id = const Value.absent(), this.content = const Value.absent(), - this.creationDate = const Value.absent(), + this.done = const Value.absent(), }); } class $TodoEntriesTable extends TodoEntries - with TableInfo<$TodoEntriesTable, TodoEntry> { + with TableInfo<$TodoEntriesTable, Entry> { final GeneratedDatabase _db; final String _alias; $TodoEntriesTable(this._db, [this._alias]); @@ -122,19 +117,17 @@ class $TodoEntriesTable extends TodoEntries ); } - final VerificationMeta _creationDateMeta = - const VerificationMeta('creationDate'); - GeneratedDateTimeColumn _creationDate; + final VerificationMeta _doneMeta = const VerificationMeta('done'); + GeneratedBoolColumn _done; @override - GeneratedDateTimeColumn get creationDate => - _creationDate ??= _constructCreationDate(); - GeneratedDateTimeColumn _constructCreationDate() { - return GeneratedDateTimeColumn('creation_date', $tableName, false, - defaultValue: currentDateAndTime); + GeneratedBoolColumn get done => _done ??= _constructDone(); + GeneratedBoolColumn _constructDone() { + return GeneratedBoolColumn('done', $tableName, false, + defaultValue: const Constant(true)); } @override - List get $columns => [id, content, creationDate]; + List get $columns => [id, content, done]; @override $TodoEntriesTable get asDslTable => this; @override @@ -156,13 +149,11 @@ class $TodoEntriesTable extends TodoEntries } else if (content.isRequired && isInserting) { context.missing(_contentMeta); } - if (d.creationDate.present) { + if (d.done.present) { context.handle( - _creationDateMeta, - creationDate.isAcceptableValue( - d.creationDate.value, _creationDateMeta)); - } else if (creationDate.isRequired && isInserting) { - context.missing(_creationDateMeta); + _doneMeta, done.isAcceptableValue(d.done.value, _doneMeta)); + } else if (done.isRequired && isInserting) { + context.missing(_doneMeta); } return context; } @@ -170,9 +161,9 @@ class $TodoEntriesTable extends TodoEntries @override Set get $primaryKey => {id}; @override - TodoEntry map(Map data, {String tablePrefix}) { + Entry map(Map data, {String tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : null; - return TodoEntry.fromData(data, _db, prefix: effectivePrefix); + return Entry.fromData(data, _db, prefix: effectivePrefix); } @override @@ -184,9 +175,8 @@ class $TodoEntriesTable extends TodoEntries if (d.content.present) { map['content'] = Variable(d.content.value); } - if (d.creationDate.present) { - map['creation_date'] = - Variable(d.creationDate.value); + if (d.done.present) { + map['done'] = Variable(d.done.value); } return map; } @@ -197,10 +187,38 @@ class $TodoEntriesTable extends TodoEntries } } +class HiddenEntryCountResult { + final int entries; + HiddenEntryCountResult({ + this.entries, + }); +} + abstract class _$Database extends GeneratedDatabase { _$Database(QueryExecutor e) : super(const SqlTypeSystem.withDefaults(), e); $TodoEntriesTable _todoEntries; $TodoEntriesTable get todoEntries => _todoEntries ??= $TodoEntriesTable(this); + HiddenEntryCountResult _rowToHiddenEntryCountResult(QueryRow row) { + return HiddenEntryCountResult( + entries: row.readInt('entries'), + ); + } + + Future> hiddenEntryCount( + {QueryEngine operateOn}) { + return (operateOn ?? this).customSelect( + 'SELECT COUNT(*) - 20 AS entries FROM todo_entries WHERE done', + variables: []).then((rows) => rows.map(_rowToHiddenEntryCountResult).toList()); + } + + Stream> watchHiddenEntryCount() { + return customSelectStream( + 'SELECT COUNT(*) - 20 AS entries FROM todo_entries WHERE done', + variables: [], + readsFrom: {todoEntries}) + .map((rows) => rows.map(_rowToHiddenEntryCountResult).toList()); + } + @override List get allTables => [todoEntries]; } diff --git a/moor/example_web/lib/main.dart b/moor/example_web/lib/main.dart new file mode 100644 index 00000000..5409163d --- /dev/null +++ b/moor/example_web/lib/main.dart @@ -0,0 +1,32 @@ +import 'package:example_web/widgets/home_screen.dart'; +import 'package:flutter_web/material.dart'; + +import 'database/database.dart'; + +void launchApp() { + runApp( + DatabaseProvider( + db: Database(), + child: MaterialApp( + title: 'Moor web!', + home: HomeScreen(), + ), + ), + ); +} + +class DatabaseProvider extends InheritedWidget { + final Database db; + + DatabaseProvider({@required this.db, @required Widget child}) + : super(child: child); + + @override + bool updateShouldNotify(InheritedWidget oldWidget) => false; + + static Database provide(BuildContext ctx) { + return (ctx.inheritFromWidgetOfExactType(DatabaseProvider) + as DatabaseProvider) + ?.db; + } +} diff --git a/moor/example_web/lib/widgets/create_entry_bar.dart b/moor/example_web/lib/widgets/create_entry_bar.dart new file mode 100644 index 00000000..369ccf3a --- /dev/null +++ b/moor/example_web/lib/widgets/create_entry_bar.dart @@ -0,0 +1,43 @@ +import 'package:flutter_web/material.dart'; + +import '../main.dart'; + +class CreateEntryBar extends StatefulWidget { + @override + _CreateEntryBarState createState() => _CreateEntryBarState(); +} + +class _CreateEntryBarState extends State { + final TextEditingController _controller = TextEditingController(); + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: TextField( + controller: _controller, + decoration: InputDecoration( + border: OutlineInputBorder(), + ), + ), + ), + IconButton( + icon: Icon(Icons.add), + onPressed: () { + final text = _controller.text; + _controller.clear(); + + DatabaseProvider.provide(context).createTodoEntry(text); + }, + ), + ], + ); + } +} diff --git a/moor/example_web/lib/widgets/entry_list.dart b/moor/example_web/lib/widgets/entry_list.dart new file mode 100644 index 00000000..e5f7df9a --- /dev/null +++ b/moor/example_web/lib/widgets/entry_list.dart @@ -0,0 +1,59 @@ +import 'package:example_web/database/database.dart'; +import 'package:flutter_web/material.dart'; + +import '../main.dart'; + +class SliverEntryList extends StatelessWidget { + final Stream> entries; + + const SliverEntryList({Key key, this.entries}) : super(key: key); + + @override + Widget build(BuildContext context) { + return StreamBuilder>( + stream: entries, + builder: (context, snapshot) { + final entries = snapshot.data ?? const []; + + return SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + final entry = entries[index]; + + return _EntryCard( + key: ObjectKey(entry.id), + entry: entry, + ); + }, + childCount: entries.length, + ), + ); + }, + ); + } +} + +class _EntryCard extends StatelessWidget { + final Entry entry; + + const _EntryCard({Key key, this.entry}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Card( + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Text(entry.content), + Spacer(), + Checkbox( + value: entry.done, + onChanged: (checked) { + DatabaseProvider.provide(context).setCompleted(entry, checked); + }, + ), + ], + ), + ); + } +} diff --git a/moor/example_web/lib/widgets/home_screen.dart b/moor/example_web/lib/widgets/home_screen.dart new file mode 100644 index 00000000..15c9e68a --- /dev/null +++ b/moor/example_web/lib/widgets/home_screen.dart @@ -0,0 +1,55 @@ +import 'package:example_web/main.dart'; +import 'package:example_web/widgets/entry_list.dart'; +import 'package:flutter_web/material.dart'; + +import 'create_entry_bar.dart'; + +class HomeScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + final db = DatabaseProvider.provide(context); + final headerTheme = Theme.of(context).textTheme.title; + + return Scaffold( + appBar: AppBar( + title: const Text('Moor Web'), + ), + body: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 800), + child: CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: CreateEntryBar(), + ), + SliverToBoxAdapter( + child: Text('In progress', style: headerTheme), + ), + SliverEntryList( + entries: db.incompleteEntries(), + ), + SliverToBoxAdapter( + child: Text('Complete', style: headerTheme), + ), + SliverEntryList( + entries: db.newestDoneEntries(), + ), + SliverToBoxAdapter( + child: StreamBuilder>( + builder: (context, snapshot) { + final hiddenCount = snapshot?.data?.single ?? 0; + + return Text('Not showing $hiddenCount older entries'); + }, + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/moor/example_web/pubspec.yaml b/moor/example_web/pubspec.yaml index ff081fa2..f5c0f4ed 100644 --- a/moor/example_web/pubspec.yaml +++ b/moor/example_web/pubspec.yaml @@ -6,6 +6,7 @@ environment: dependencies: moor: ^1.4.0 + flutter_web: any dev_dependencies: moor_generator: @@ -17,3 +18,12 @@ dependency_overrides: path: ../ moor_generator: path: ../../moor_generator + + flutter_web_ui: + git: + url: https://github.com/flutter/flutter_web.git + path: packages/flutter_web_ui + flutter_web: + git: + url: https://github.com/flutter/flutter_web.git + path: packages/flutter_web \ No newline at end of file diff --git a/moor/example_web/web/assets/FontManifest.json b/moor/example_web/web/assets/FontManifest.json new file mode 100644 index 00000000..cd2d0d55 --- /dev/null +++ b/moor/example_web/web/assets/FontManifest.json @@ -0,0 +1,10 @@ +[ + { + "family": "MaterialIcons", + "fonts": [ + { + "asset": "https://fonts.gstatic.com/s/materialicons/v42/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2" + } + ] + } +] \ No newline at end of file diff --git a/moor/example_web/web/database.dart b/moor/example_web/web/database.dart deleted file mode 100644 index 004a113a..00000000 --- a/moor/example_web/web/database.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:moor/moor.dart'; - -part 'database.g.dart'; - -@DataClassName('TodoEntry') -class TodoEntries extends Table { - IntColumn get id => integer().autoIncrement()(); - - TextColumn get content => text()(); - DateTimeColumn get creationDate => - dateTime().withDefault(currentDateAndTime)(); -} - -@UseMoor(tables: [TodoEntries]) -class Database extends _$Database { - Database(QueryExecutor e) : super(e); - - @override - int get schemaVersion => 1; - - Stream> watchEntries() { - return select(todoEntries).watch(); - } - - Future insert(String text) { - return into(todoEntries).insert(TodoEntriesCompanion(content: Value(text))); - } -} diff --git a/moor/example_web/web/index.html b/moor/example_web/web/index.html index c7b116fa..87365210 100644 --- a/moor/example_web/web/index.html +++ b/moor/example_web/web/index.html @@ -1,27 +1,11 @@ - - - + + - - - - - example_web - - - - + + + - - -
- -
- - -
- - + \ No newline at end of file diff --git a/moor/example_web/web/main.dart b/moor/example_web/web/main.dart index 93b5bbc9..ff03b881 100644 --- a/moor/example_web/web/main.dart +++ b/moor/example_web/web/main.dart @@ -1,18 +1,7 @@ -import 'dart:html'; - -import 'package:moor/moor_web.dart'; - -import 'database.dart'; +import 'package:example_web/main.dart'; +import 'package:flutter_web_ui/ui.dart' as ui; void main() async { - final db = Database(WebDatabase('database', logStatements: true)); - db.watchEntries().listen(print); - - (querySelector('#add_todo_form') as FormElement).onSubmit.listen((e) { - final content = querySelector('#description') as InputElement; - e.preventDefault(); - - db.insert(content.value).then((insertId) => print('inserted #$insertId')); - content.value = ''; - }); + await ui.webOnlyInitializePlatform(); + launchApp(); } diff --git a/moor/example_web/web/styles.css b/moor/example_web/web/styles.css deleted file mode 100644 index cc035c95..00000000 --- a/moor/example_web/web/styles.css +++ /dev/null @@ -1,14 +0,0 @@ -@import url(https://fonts.googleapis.com/css?family=Roboto); - -html, body { - width: 100%; - height: 100%; - margin: 0; - padding: 0; - font-family: 'Roboto', sans-serif; -} - -#output { - padding: 20px; - text-align: center; -} diff --git a/moor/lib/src/web/web_db.dart b/moor/lib/src/web/web_db.dart index 4edf20ed..6855094b 100644 --- a/moor/lib/src/web/web_db.dart +++ b/moor/lib/src/web/web_db.dart @@ -223,6 +223,11 @@ class WebDatabase extends _DatabaseUser { return databaseInfo.beforeOpenCallback(_BeforeOpenExecutor(_state), OpeningDetails(version, databaseInfo.schemaVersion)); }); + + if (upgradeNeeded) { + // assume that a schema version was written in an upgrade => save db + _storeDb(); + } } }