stock_home.dart 10.6 KB
Newer Older
1 2 3 4
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'package:flutter/material.dart';
6
import 'package:flutter/rendering.dart' show debugDumpRenderTree, debugDumpLayerTree, debugDumpSemanticsTree, DebugSemanticsDumpOrder;
7
import 'package:flutter/scheduler.dart' show timeDilation;
8
import 'package:flutter/gestures.dart' show DragStartBehavior;
9 10 11 12 13
import 'stock_data.dart';
import 'stock_list.dart';
import 'stock_strings.dart';
import 'stock_symbol_viewer.dart';
import 'stock_types.dart';
14

15
typedef ModeUpdater = void Function(StockMode mode);
16

17
enum _StockMenuItem { autorefresh, refresh, speedUp, speedDown }
18 19
enum StockHomeTab { market, portfolio }

20
class _NotImplementedDialog extends StatelessWidget {
21
  @override
22
  Widget build(BuildContext context) {
23
    return AlertDialog(
24 25
      title: const Text('Not Implemented'),
      content: const Text('This feature has not yet been implemented.'),
26
      actions: <Widget>[
27
        FlatButton(
28
          onPressed: debugDumpApp,
29
          child: Row(
30
            children: <Widget>[
31
              const Icon(
Ian Hickson's avatar
Ian Hickson committed
32
                Icons.dvr,
33
                size: 18.0,
34
              ),
35
              Container(
36
                width: 8.0,
37
              ),
38
              const Text('DUMP APP TO CONSOLE'),
39 40
            ],
          ),
41
        ),
42
        FlatButton(
43 44
          onPressed: () {
            Navigator.pop(context, false);
Ian Hickson's avatar
Ian Hickson committed
45
          },
46
          child: const Text('OH WELL'),
47 48
        ),
      ],
49 50 51 52
    );
  }
}

53
class StockHome extends StatefulWidget {
54
  const StockHome(this.stocks, this.configuration, this.updater);
55

56
  final StockData stocks;
57 58
  final StockConfiguration configuration;
  final ValueChanged<StockConfiguration> updater;
59

60
  @override
61
  StockHomeState createState() => StockHomeState();
62 63 64
}

class StockHomeState extends State<StockHome> {
65 66
  final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
  final TextEditingController _searchQuery = TextEditingController();
67
  bool _isSearching = false;
68
  bool _autorefresh = false;
69

70
  void _handleSearchBegin() {
71
    ModalRoute.of(context).addLocalHistoryEntry(LocalHistoryEntry(
Hixie's avatar
Hixie committed
72
      onRemove: () {
Adam Barth's avatar
Adam Barth committed
73 74
        setState(() {
          _isSearching = false;
75
          _searchQuery.clear();
Adam Barth's avatar
Adam Barth committed
76
        });
77
      },
Adam Barth's avatar
Adam Barth committed
78
    ));
79 80 81 82 83
    setState(() {
      _isSearching = true;
    });
  }

Adam Barth's avatar
Adam Barth committed
84
  void _handleStockModeChange(StockMode value) {
85 86
    if (widget.updater != null)
      widget.updater(widget.configuration.copyWith(stockMode: value));
87 88
  }

89
  void _handleStockMenu(BuildContext context, _StockMenuItem value) {
90
    switch (value) {
91 92 93 94 95 96
      case _StockMenuItem.autorefresh:
        setState(() {
          _autorefresh = !_autorefresh;
        });
        break;
      case _StockMenuItem.refresh:
97
        showDialog<void>(
98
          context: context,
99
          builder: (BuildContext context) => _NotImplementedDialog(),
100 101 102 103 104 105 106 107 108
        );
        break;
      case _StockMenuItem.speedUp:
        timeDilation /= 5.0;
        break;
      case _StockMenuItem.speedDown:
        timeDilation *= 5.0;
        break;
    }
109 110
  }

111
  Widget _buildDrawer(BuildContext context) {
112 113
    return Drawer(
      child: ListView(
114
        dragStartBehavior: DragStartBehavior.down,
115
        children: <Widget>[
116
          const DrawerHeader(child: Center(child: Text('Stocks'))),
117
          const ListTile(
118 119
            leading: Icon(Icons.assessment),
            title: Text('Stock List'),
120 121
            selected: true,
          ),
122
          const ListTile(
123 124
            leading: Icon(Icons.account_balance),
            title: Text('Account Balance'),
125
            enabled: false,
126
          ),
127
          ListTile(
128 129
            leading: const Icon(Icons.dvr),
            title: const Text('Dump App to Console'),
130
            onTap: () {
131 132 133 134
              try {
                debugDumpApp();
                debugDumpRenderTree();
                debugDumpLayerTree();
135
                debugDumpSemanticsTree(DebugSemanticsDumpOrder.traversalOrder);
136 137 138 139 140
              } catch (e, stack) {
                debugPrint('Exception while dumping app:\n$e\n$stack');
              }
            },
          ),
141
          const Divider(),
142
          ListTile(
143 144
            leading: const Icon(Icons.thumb_up),
            title: const Text('Optimistic'),
145
            trailing: Radio<StockMode>(
146
              value: StockMode.optimistic,
147
              groupValue: widget.configuration.stockMode,
148
              onChanged: _handleStockModeChange,
149 150 151 152
            ),
            onTap: () {
              _handleStockModeChange(StockMode.optimistic);
            },
153
          ),
154
          ListTile(
155 156
            leading: const Icon(Icons.thumb_down),
            title: const Text('Pessimistic'),
157
            trailing: Radio<StockMode>(
158
              value: StockMode.pessimistic,
159
              groupValue: widget.configuration.stockMode,
160
              onChanged: _handleStockModeChange,
161 162 163 164
            ),
            onTap: () {
              _handleStockModeChange(StockMode.pessimistic);
            },
165
          ),
166
          const Divider(),
167
          ListTile(
168 169
            leading: const Icon(Icons.settings),
            title: const Text('Settings'),
170 171
            onTap: _handleShowSettings,
          ),
172
          ListTile(
173 174
            leading: const Icon(Icons.help),
            title: const Text('About'),
175 176
            onTap: _handleShowAbout,
          ),
177 178
        ],
      ),
179 180 181
    );
  }

Adam Barth's avatar
Adam Barth committed
182
  void _handleShowSettings() {
Hixie's avatar
Hixie committed
183
    Navigator.popAndPushNamed(context, '/settings');
184 185
  }

186 187 188 189
  void _handleShowAbout() {
    showAboutDialog(context: context);
  }

190
  Widget buildAppBar() {
191
    return AppBar(
192
      elevation: 0.0,
193
      title: Text(StockStrings.of(context).title()),
194
      actions: <Widget>[
195
        IconButton(
196
          icon: const Icon(Icons.search),
Hixie's avatar
Hixie committed
197
          onPressed: _handleSearchBegin,
198
          tooltip: 'Search',
199
        ),
200
        PopupMenuButton<_StockMenuItem>(
201
          onSelected: (_StockMenuItem value) { _handleStockMenu(context, value); },
202
          itemBuilder: (BuildContext context) => <PopupMenuItem<_StockMenuItem>>[
203
            CheckedPopupMenuItem<_StockMenuItem>(
204
              value: _StockMenuItem.autorefresh,
205
              checked: _autorefresh,
206
              child: const Text('Autorefresh'),
207
            ),
208
            const PopupMenuItem<_StockMenuItem>(
209
              value: _StockMenuItem.refresh,
210
              child: Text('Refresh'),
211
            ),
212
            const PopupMenuItem<_StockMenuItem>(
213
              value: _StockMenuItem.speedUp,
214
              child: Text('Increase animation speed'),
215
            ),
216
            const PopupMenuItem<_StockMenuItem>(
217
              value: _StockMenuItem.speedDown,
218
              child: Text('Decrease animation speed'),
219 220 221
            ),
          ],
        ),
222
      ],
223
      bottom: TabBar(
Hans Muller's avatar
Hans Muller committed
224
        tabs: <Widget>[
225 226
          Tab(text: StockStrings.of(context).market()),
          Tab(text: StockStrings.of(context).portfolio()),
227 228
        ],
      ),
229
    );
230 231
  }

232 233
  static Iterable<Stock> _getStockList(StockData stocks, Iterable<String> symbols) {
    return symbols.map<Stock>((String symbol) => stocks[symbol])
234
        .where((Stock stock) => stock != null);
235 236 237
  }

  Iterable<Stock> _filterBySearchQuery(Iterable<Stock> stocks) {
238
    if (_searchQuery.text.isEmpty)
239
      return stocks;
240
    final RegExp regexp = RegExp(_searchQuery.text, caseSensitive: false);
Hixie's avatar
Hixie committed
241
    return stocks.where((Stock stock) => stock.symbol.contains(regexp));
242 243
  }

244
  void _buyStock(Stock stock) {
Hixie's avatar
Hixie committed
245 246 247 248
    setState(() {
      stock.percentChange = 100.0 * (1.0 / stock.lastSale);
      stock.lastSale += 1.0;
    });
249 250 251
    _scaffoldKey.currentState.showSnackBar(SnackBar(
      content: Text('Purchased ${stock.symbol} for ${stock.lastSale}'),
      action: SnackBarAction(
252
        label: 'BUY MORE',
253
        onPressed: () {
254
          _buyStock(stock);
255 256
        },
      ),
Hixie's avatar
Hixie committed
257 258 259
    ));
  }

260
  Widget _buildStockList(BuildContext context, Iterable<Stock> stocks, StockHomeTab tab) {
261
    return StockList(
262
      stocks: stocks.toList(),
Hixie's avatar
Hixie committed
263
      onAction: _buyStock,
264
      onOpen: (Stock stock) {
265
        Navigator.pushNamed(context, '/stock', arguments: stock.symbol);
266
      },
267
      onShow: (Stock stock) {
268
        _scaffoldKey.currentState.showBottomSheet<void>((BuildContext context) => StockSymbolBottomSheet(stock: stock));
269
      },
270
    );
271 272
  }

273
  Widget _buildStockTab(BuildContext context, StockHomeTab tab, List<String> stockSymbols) {
274 275 276
    return AnimatedBuilder(
      key: ValueKey<StockHomeTab>(tab),
      animation: Listenable.merge(<Listenable>[_searchQuery, widget.stocks]),
277 278 279
      builder: (BuildContext context, Widget child) {
        return _buildStockList(context, _filterBySearchQuery(_getStockList(widget.stocks, stockSymbols)).toList(), tab);
      },
280 281 282
    );
  }

283
  static const List<String> portfolioSymbols = <String>['AAPL','FIZZ', 'FIVE', 'FLAT', 'ZINC', 'ZNGA'];
Hixie's avatar
Hixie committed
284

285
  Widget buildSearchBar() {
286 287
    return AppBar(
      leading: BackButton(
Adam Barth's avatar
Adam Barth committed
288
        color: Theme.of(context).accentColor,
289
      ),
290
      title: TextField(
291
        controller: _searchQuery,
292
        autofocus: true,
293 294 295
        decoration: const InputDecoration(
          hintText: 'Search stocks',
        ),
296
      ),
297
      backgroundColor: Theme.of(context).canvasColor,
298 299 300
    );
  }

Hixie's avatar
Hixie committed
301
  void _handleCreateCompany() {
302
    showModalBottomSheet<void>(
Adam Barth's avatar
Adam Barth committed
303
      context: context,
304
      builder: (BuildContext context) => _CreateCompanySheet(),
Matt Perry's avatar
Matt Perry committed
305
    );
306 307 308
  }

  Widget buildFloatingActionButton() {
309
    return FloatingActionButton(
310
      tooltip: 'Create company',
311
      child: const Icon(Icons.add),
312
      backgroundColor: Theme.of(context).accentColor,
313
      onPressed: _handleCreateCompany,
314
    );
315 316
  }

317
  @override
318
  Widget build(BuildContext context) {
319
    return DefaultTabController(
Hans Muller's avatar
Hans Muller committed
320
      length: 2,
321
      child: Scaffold(
322
        drawerDragStartBehavior: DragStartBehavior.down,
323
        key: _scaffoldKey,
324
        appBar: _isSearching ? buildSearchBar() : buildAppBar(),
325 326
        floatingActionButton: buildFloatingActionButton(),
        drawer: _buildDrawer(context),
327
        body: TabBarView(
328
          dragStartBehavior: DragStartBehavior.down,
Adam Barth's avatar
Adam Barth committed
329
          children: <Widget>[
330
            _buildStockTab(context, StockHomeTab.market, widget.stocks.allSymbols),
Adam Barth's avatar
Adam Barth committed
331
            _buildStockTab(context, StockHomeTab.portfolio, portfolioSymbols),
332 333 334
          ],
        ),
      ),
335
    );
336 337
  }
}
338

339
class _CreateCompanySheet extends StatelessWidget {
340
  @override
341
  Widget build(BuildContext context) {
342
    return Column(
343
      children: const <Widget>[
344
        TextField(
345
          autofocus: true,
346
          decoration: InputDecoration(
347 348
            hintText: 'Company Name',
          ),
349
        ),
350
        Text('(This demo is not yet complete.)'),
351 352
        // For example, we could add a button that actually updates the list
        // and then contacts the server, etc.
353
      ],
354 355 356
    );
  }
}