import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:firebase_database/firebase_database.dart'; import 'package:flutter/rendering.dart'; import 'package:rxdart/rxdart.dart'; import 'dart:async'; void main() { Firestore.instance.enablePersistence(true); FirebaseDatabase.instance.setPersistenceEnabled(false); runApp(MaterialApp(title: "Happy.do", home: HappyDo())); } class HappyDo extends StatelessWidget { @override Widget build(BuildContext context) { return DefaultTabController( length: 3, child: Scaffold( appBar: AppBar( title: Text('Happy.do'), bottom: TabBar( tabs: [ Tab( text: 'Today', ), Tab(text: 'This week'), Tab(text: 'Next week'), ], )), body: TabBarView( children: [ ReordableTodoListView( stream: Observable(Firestore.instance .collection('todos') .where('date', isEqualTo: '2018-09-25') .where('weekly', isEqualTo: false) .snapshots()), ), ReordableTodoListView( stream: Observable(Firestore.instance .collection('todos') .where('date', isGreaterThanOrEqualTo: '2018-09-24') .where('date', isLessThanOrEqualTo: '2018-09-30') .where('weekly', isEqualTo: false) .snapshots()), ), ReordableTodoListView( stream: Observable(Firestore.instance .collection('todos') .where('date', isGreaterThanOrEqualTo: '2018-10-01') .where('date', isLessThanOrEqualTo: '2018-10-07') .where('weekly', isEqualTo: true) .snapshots()), ), ], ), floatingActionButton: FloatingActionButton( child: Icon(Icons.add), onPressed: () async { await Firestore.instance.collection('todos').document().setData({ 'order': -DateTime.now().millisecondsSinceEpoch.toDouble(), 'todo': '', 'date': '2018-09-25', 'weekly': false, 'checked': false }); }, ), ), ); } } class ReordableTodoListView extends StatelessWidget { final Observable stream; ReordableTodoListView({Key key, @required this.stream}) : super(key: key); @override Widget build(BuildContext context) { return StreamBuilder>( stream: stream.map((s) => s.documents ..sort((d1, d2) => ((d1.data['order'] ?? 0) - (d2.data['order'] ?? 0)).toInt())), builder: (context, documents) { if (documents.data == null) return Container(); final orderMutation = List(); // If we want to drag&drop element X between elements Y&Z // that's enough to change only X.order to (Y.order+Z.order)*0.5 // // If we want to move element to first or to last position // we use -current_time and +current as not the ideal but // prototype valid solution. // // The approach itself is inspired by Trello API. for (var i = 0; i < documents.data.length - 1; i++) { final order1 = documents.data[i]['order'] ?? 0.0; final order2 = documents.data[i + 1]['order'] ?? 0.0; orderMutation.add((order1 + order2) * 0.5); } orderMutation.insert(0, -DateTime.now().millisecondsSinceEpoch.toDouble()); orderMutation.insert(orderMutation.length, DateTime.now().millisecondsSinceEpoch.toDouble()); debugPrint('hackList = $orderMutation'); documents.data.toList().fold>( [-DateTime.now().millisecondsSinceEpoch.toDouble()].toList(), (list, order) { return list; }); return Container( child: ReorderableListView( padding: EdgeInsets.all(8.0), onReorder: (oldIndex, newIndex) async { debugPrint('$oldIndex, $newIndex'); await Firestore.instance .collection('todos') .document(documents.data[oldIndex].documentID) .updateData({'order': orderMutation[newIndex]}); }, children: documents.data .map((document) { return TodoWidget( key: ValueKey(document.documentID), checked: document['checked'], text: document['todo'], documentId: document .documentID, //todo: try to pass text & checked here? );}) .toList()), // child: Column( // children: documents.data // .map((document) => TodoWidget( // key: GlobalObjectKey(document.documentID.hashCode), // documentId: document.documentID, // )) // .toList()), ); }); } } class TodoListWidget extends StatefulWidget { final String title; final Observable> stream; final Function addPressed; TodoListWidget({Key key, this.title, this.stream, this.addPressed}) : super(key: key); @override _TodoListWidgetState createState() => _TodoListWidgetState(); } class _TodoListWidgetState extends State { @override Widget build(BuildContext context) { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.only(left: 16.0), child: Row( children: [ Flexible( child: Text( widget.title, style: TextStyle( fontSize: 20.0, fontWeight: FontWeight.bold, ), ), ), IconButton( icon: Icon(Icons.add), onPressed: () { widget.addPressed(); }, ), ], ), ), StreamBuilder>( stream: widget.stream, builder: (context, documents) { if (documents.data == null) return Container(); return Padding( padding: const EdgeInsets.symmetric(vertical: 16.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: List() ..addAll(documents.data .map((documentSnapshot) { return TodoWidget( key: Key(documentSnapshot.documentID), checked: documentSnapshot["checked"], documentId: documentSnapshot.documentID); }) .fold>( List()..add(Text('123')), (current, widget) => current..addAll([widget, Text('123')])) .toList()))); }, ), ], ); }); } } class TodoWidget extends StatefulWidget { final String documentId; final String text; final bool checked; TodoWidget({Key key, this.documentId, this.text = "", this.checked = false}) : super(key: key); @override _TodoWidgetState createState() => _TodoWidgetState(); } class _TodoWidgetState extends State { MyTextEditingController outController = MyTextEditingController(text: ''); BehaviorSubject subject; StreamSubscription subscription; bool checked = false; @override void initState() { super.initState(); this.checked = widget.checked; this.outController.text = widget.text; debugPrint('initState $widget.documentId, $checked, ${widget.text}'); subscription = Firestore.instance .collection('todos') .document(widget.documentId) .snapshots() .listen((documentSnapshot) { setState(() { outController.text = documentSnapshot.exists ? documentSnapshot["todo"] : "[none]"; checked = documentSnapshot.exists ? documentSnapshot["checked"] ?? false : false; }); }); subject = BehaviorSubject(); subject.debounce(Duration(milliseconds: 750)).listen((text) { Firestore.instance .collection('todos') .document(widget.documentId) .updateData({'todo': text, 'checked': checked}); }); } @override void didUpdateWidget(TodoWidget oldWidget) { debugPrint('didUpdateWidget ' + widget.documentId + ' from ' + oldWidget.documentId); super.didUpdateWidget(oldWidget); } @override void dispose() { debugPrint('dispose ' + widget.documentId); subject.close(); subscription.cancel(); super.dispose(); } @override Widget build(BuildContext context) { return _buildTodoRow(); } Row _buildTodoRow() { return Row( children: [ Checkbox( value: (outController.text.trim() != '') ? checked : false, onChanged: (changedChecked) async { if (outController.text.trim() != '') { // TODO: cancel deferred events on subject await Firestore.instance .collection('todos') .document(widget.documentId) .updateData( {'checked': changedChecked, 'todo': outController.text}); } }, ), Flexible( child: TextField( controller: outController, onChanged: (text) { subject.add(text); }, style: ((outController.text.trim() != '') ? checked : false) ? TextStyle( color: Colors.black, decoration: TextDecoration.lineThrough) : TextStyle(color: Colors.black), enabled: !(((outController.text.trim() != '') ? checked : false)), decoration: InputDecoration( border: InputBorder.none, hintText: "enter new todo here"), ), ), IconButton( icon: Icon(Icons.code), ), IconButton( icon: Icon(Icons.delete), onPressed: () async { await Firestore.instance .collection('todos') .document(widget.documentId) .delete(); }, ) ], ); } } /// workaround for https://github.com/flutter/flutter/issues/22171 class MyTextEditingController extends TextEditingController { MyTextEditingController({String text}) : super(text: text); set text(String newText) { try { value = value.copyWith( text: newText); } catch (e) { value = value.copyWith( text: newText, selection: TextSelection.collapsed(offset: -1), composing: TextRange.empty); } } }