Skip to content

Instantly share code, notes, and snippets.

@AlexKorovyansky
Last active September 26, 2018 18:10
Show Gist options
  • Select an option

  • Save AlexKorovyansky/066f69bba0b7fa5affed49f4b444e6e4 to your computer and use it in GitHub Desktop.

Select an option

Save AlexKorovyansky/066f69bba0b7fa5affed49f4b444e6e4 to your computer and use it in GitHub Desktop.

Revisions

  1. AlexKorovyansky revised this gist Sep 26, 2018. 1 changed file with 66 additions and 59 deletions.
    125 changes: 66 additions & 59 deletions main.dart
    Original file line number Diff line number Diff line change
    @@ -90,11 +90,11 @@ class ReordableTodoListView extends StatelessWidget {
    final orderMutation = List<double>();
    // 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;
    @@ -107,9 +107,9 @@ class ReordableTodoListView extends StatelessWidget {
    debugPrint('hackList = $orderMutation');
    documents.data.toList().fold<List<double>>(
    [-DateTime.now().millisecondsSinceEpoch.toDouble()].toList(),
    (list, order) {
    return list;
    });
    (list, order) {
    return list;
    });
    return Container(
    child: ReorderableListView(
    padding: EdgeInsets.all(8.0),
    @@ -121,11 +121,13 @@ class ReordableTodoListView extends StatelessWidget {
    .updateData({'order': orderMutation[newIndex]});
    },
    children: documents.data
    .map((document) => TodoWidget(
    key: ValueKey(document.documentID),
    documentId: document
    .documentID, //todo: try to pass text & checked here?
    ))
    .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
    @@ -156,64 +158,67 @@ class _TodoListWidgetState extends State<TodoListWidget> {
    Widget build(BuildContext context) {
    return LayoutBuilder(
    builder: (BuildContext context, BoxConstraints constraints) {
    return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: <Widget>[
    Padding(
    padding: const EdgeInsets.only(left: 16.0),
    child: Row(
    children: <Widget>[
    Flexible(
    child: Text(
    widget.title,
    style: TextStyle(
    fontSize: 20.0,
    fontWeight: FontWeight.bold,
    return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: <Widget>[
    Padding(
    padding: const EdgeInsets.only(left: 16.0),
    child: Row(
    children: <Widget>[
    Flexible(
    child: Text(
    widget.title,
    style: TextStyle(
    fontSize: 20.0,
    fontWeight: FontWeight.bold,
    ),
    ),
    ),
    ),
    ),
    IconButton(
    icon: Icon(Icons.add),
    onPressed: () {
    widget.addPressed();
    },
    IconButton(
    icon: Icon(Icons.add),
    onPressed: () {
    widget.addPressed();
    },
    ),
    ],
    ),
    ],
    ),
    ),
    StreamBuilder<List<dynamic>>(
    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<Widget>()
    ..addAll(documents.data
    .map((documentSnapshot) {
    ),
    StreamBuilder<List<dynamic>>(
    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<Widget>()
    ..addAll(documents.data
    .map((documentSnapshot) {
    return TodoWidget(
    key: Key(documentSnapshot.documentID),
    checked: documentSnapshot["checked"],
    documentId: documentSnapshot.documentID);
    })
    .fold<List<Widget>>(
    .fold<List<Widget>>(
    List<Widget>()..add(Text('123')),
    (current, widget) =>
    current..addAll([widget, Text('123')]))
    .toList())));
    },
    ),
    ],
    );
    });
    (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}) : super(key: key);
    TodoWidget({Key key, this.documentId, this.text = "", this.checked = false}) : super(key: key);

    @override
    _TodoWidgetState createState() => _TodoWidgetState();
    @@ -228,15 +233,17 @@ class _TodoWidgetState extends State<TodoWidget> {
    @override
    void initState() {
    super.initState();
    debugPrint('initState ' + widget.documentId);
    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]";
    documentSnapshot.exists ? documentSnapshot["todo"] : "[none]";
    checked = documentSnapshot.exists
    ? documentSnapshot["checked"] ?? false
    : false;
    @@ -285,7 +292,7 @@ class _TodoWidgetState extends State<TodoWidget> {
    .collection('todos')
    .document(widget.documentId)
    .updateData(
    {'checked': changedChecked, 'todo': outController.text});
    {'checked': changedChecked, 'todo': outController.text});
    }
    },
    ),
    @@ -297,7 +304,7 @@ class _TodoWidgetState extends State<TodoWidget> {
    },
    style: ((outController.text.trim() != '') ? checked : false)
    ? TextStyle(
    color: Colors.black, decoration: TextDecoration.lineThrough)
    color: Colors.black, decoration: TextDecoration.lineThrough)
    : TextStyle(color: Colors.black),
    enabled: !(((outController.text.trim() != '') ? checked : false)),
    decoration: InputDecoration(
    @@ -338,4 +345,4 @@ class MyTextEditingController extends TextEditingController {
    composing: TextRange.empty);
    }
    }
    }
    }
  2. AlexKorovyansky created this gist Sep 26, 2018.
    341 changes: 341 additions & 0 deletions main.dart
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,341 @@
    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: <Widget>[
    ReordableTodoListView(
    stream: Observable<QuerySnapshot>(Firestore.instance
    .collection('todos')
    .where('date', isEqualTo: '2018-09-25')
    .where('weekly', isEqualTo: false)
    .snapshots()),
    ),
    ReordableTodoListView(
    stream: Observable<QuerySnapshot>(Firestore.instance
    .collection('todos')
    .where('date', isGreaterThanOrEqualTo: '2018-09-24')
    .where('date', isLessThanOrEqualTo: '2018-09-30')
    .where('weekly', isEqualTo: false)
    .snapshots()),
    ),
    ReordableTodoListView(
    stream: Observable<QuerySnapshot>(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<QuerySnapshot> stream;

    ReordableTodoListView({Key key, @required this.stream}) : super(key: key);

    @override
    Widget build(BuildContext context) {
    return StreamBuilder<List<DocumentSnapshot>>(
    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<double>();
    // 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<List<double>>(
    [-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) => TodoWidget(
    key: ValueKey(document.documentID),
    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<List<dynamic>> stream;
    final Function addPressed;

    TodoListWidget({Key key, this.title, this.stream, this.addPressed})
    : super(key: key);

    @override
    _TodoListWidgetState createState() => _TodoListWidgetState();
    }

    class _TodoListWidgetState extends State<TodoListWidget> {
    @override
    Widget build(BuildContext context) {
    return LayoutBuilder(
    builder: (BuildContext context, BoxConstraints constraints) {
    return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: <Widget>[
    Padding(
    padding: const EdgeInsets.only(left: 16.0),
    child: Row(
    children: <Widget>[
    Flexible(
    child: Text(
    widget.title,
    style: TextStyle(
    fontSize: 20.0,
    fontWeight: FontWeight.bold,
    ),
    ),
    ),
    IconButton(
    icon: Icon(Icons.add),
    onPressed: () {
    widget.addPressed();
    },
    ),
    ],
    ),
    ),
    StreamBuilder<List<dynamic>>(
    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<Widget>()
    ..addAll(documents.data
    .map((documentSnapshot) {
    return TodoWidget(
    key: Key(documentSnapshot.documentID),
    documentId: documentSnapshot.documentID);
    })
    .fold<List<Widget>>(
    List<Widget>()..add(Text('123')),
    (current, widget) =>
    current..addAll([widget, Text('123')]))
    .toList())));
    },
    ),
    ],
    );
    });
    }
    }


    class TodoWidget extends StatefulWidget {
    final String documentId;

    TodoWidget({Key key, this.documentId}) : super(key: key);

    @override
    _TodoWidgetState createState() => _TodoWidgetState();
    }

    class _TodoWidgetState extends State<TodoWidget> {
    MyTextEditingController outController = MyTextEditingController(text: '');
    BehaviorSubject<String> subject;
    StreamSubscription<DocumentSnapshot> subscription;
    bool checked = false;

    @override
    void initState() {
    super.initState();
    debugPrint('initState ' + widget.documentId);
    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<String>();
    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: <Widget>[
    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);
    }
    }
    }