stock_home.dart 11.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 6 7 8
import 'dart:collection';

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart' show debugDumpRenderTree, debugDumpLayerTree, debugDumpSemanticsTree;
9
import 'package:flutter/scheduler.dart' show timeDilation;
10 11 12 13 14
import 'stock_data.dart';
import 'stock_list.dart';
import 'stock_strings.dart';
import 'stock_symbol_viewer.dart';
import 'stock_types.dart';
15 16 17

typedef void ModeUpdater(StockMode mode);

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

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

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

Hixie's avatar
Hixie committed
57 58
  final Map<String, Stock> stocks;
  final List<String> symbols;
59 60
  final StockConfiguration configuration;
  final ValueChanged<StockConfiguration> updater;
61

62
  @override
63 64 65 66
  StockHomeState createState() => new StockHomeState();
}

class StockHomeState extends State<StockHome> {
67

68
  final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
69
  bool _isSearching = false;
70
  InputValue _searchQuery = InputValue.empty;
71
  bool _autorefresh = false;
72

73
  void _handleSearchBegin() {
Hixie's avatar
Hixie committed
74 75
    ModalRoute.of(context).addLocalHistoryEntry(new LocalHistoryEntry(
      onRemove: () {
Adam Barth's avatar
Adam Barth committed
76 77
        setState(() {
          _isSearching = false;
78
          _searchQuery = InputValue.empty;
Adam Barth's avatar
Adam Barth committed
79 80 81
        });
      }
    ));
82 83 84 85 86 87
    setState(() {
      _isSearching = true;
    });
  }

  void _handleSearchEnd() {
Hixie's avatar
Hixie committed
88
    Navigator.pop(context);
89 90
  }

91
  void _handleSearchQueryChanged(InputValue query) {
92 93 94 95 96
    setState(() {
      _searchQuery = query;
    });
  }

Adam Barth's avatar
Adam Barth committed
97
  void _handleStockModeChange(StockMode value) {
98 99
    if (config.updater != null)
      config.updater(config.configuration.copyWith(stockMode: value));
100 101
  }

102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
  void _handleStockMenu(BuildContext context, _StockMenuItem value) {
    switch(value) {
      case _StockMenuItem.autorefresh:
        setState(() {
          _autorefresh = !_autorefresh;
        });
        break;
      case _StockMenuItem.refresh:
        showDialog(
          context: context,
          child: new _NotImplementedDialog()
        );
        break;
      case _StockMenuItem.speedUp:
        timeDilation /= 5.0;
        break;
      case _StockMenuItem.speedDown:
        timeDilation *= 5.0;
        break;
    }
122 123
  }

124 125
  Widget _buildDrawer(BuildContext context) {
    return new Drawer(
126
      child: new Block(children: <Widget>[
127
        new DrawerHeader(child: new Text('Stocks')),
128
        new DrawerItem(
129
          icon: Icons.assessment,
130
          selected: true,
131 132
          child: new Text('Stock List')
        ),
133
        new DrawerItem(
134
          icon: Icons.account_balance,
135
          onPressed: () {
Adam Barth's avatar
Adam Barth committed
136 137 138
            showDialog(
              context: context,
              child: new Dialog(
139 140
                title: new Text('Not Implemented'),
                content: new Text('This feature has not yet been implemented.'),
Hixie's avatar
Hixie committed
141
                actions: <Widget>[
142 143
                  new FlatButton(
                    onPressed: () {
Hixie's avatar
Hixie committed
144
                      Navigator.pop(context, false);
Hixie's avatar
Hixie committed
145 146
                    },
                    child: new Text('USE IT')
147 148 149
                  ),
                  new FlatButton(
                    onPressed: () {
Hixie's avatar
Hixie committed
150
                      Navigator.pop(context, false);
Hixie's avatar
Hixie committed
151 152
                    },
                    child: new Text('OH WELL')
153 154
                  ),
                ]
Adam Barth's avatar
Adam Barth committed
155 156
              )
            );
157
          },
158 159
          child: new Text('Account Balance')
        ),
160
        new DrawerItem(
161
          icon: Icons.dvr,
Hixie's avatar
Hixie committed
162 163 164 165 166 167 168 169 170 171
          onPressed: () {
            try {
              debugDumpApp();
              debugDumpRenderTree();
              debugDumpLayerTree();
              debugDumpSemanticsTree();
            } catch (e, stack) {
              debugPrint('Exception while dumping app:\n$e\n$stack');
            }
          },
172 173
          child: new Text('Dump App to Console')
        ),
Hans Muller's avatar
Hans Muller committed
174
        new Divider(),
175
        new DrawerItem(
176
          icon: Icons.thumb_up,
177
          onPressed: () => _handleStockModeChange(StockMode.optimistic),
178 179 180 181 182 183
          child: new Row(
            children: <Widget>[
              new Flexible(child: new Text('Optimistic')),
              new Radio<StockMode>(value: StockMode.optimistic, groupValue: config.configuration.stockMode, onChanged: _handleStockModeChange)
            ]
          )
184
        ),
185
        new DrawerItem(
186
          icon: Icons.thumb_down,
187
          onPressed: () => _handleStockModeChange(StockMode.pessimistic),
188 189 190 191 192 193
          child: new Row(
            children: <Widget>[
              new Flexible(child: new Text('Pessimistic')),
              new Radio<StockMode>(value: StockMode.pessimistic, groupValue: config.configuration.stockMode, onChanged: _handleStockModeChange)
            ]
          )
194
        ),
Hans Muller's avatar
Hans Muller committed
195
        new Divider(),
196
        new DrawerItem(
197
          icon: Icons.settings,
198
          onPressed: _handleShowSettings,
199
          child: new Text('Settings')),
200
        new DrawerItem(
201
          icon: Icons.help,
202
          child: new Text('Help & Feedback'))
203
      ])
204 205 206
    );
  }

Adam Barth's avatar
Adam Barth committed
207
  void _handleShowSettings() {
Hixie's avatar
Hixie committed
208
    Navigator.popAndPushNamed(context, '/settings');
209 210
  }

211 212
  Widget buildAppBar() {
    return new AppBar(
Hans Muller's avatar
Hans Muller committed
213
      elevation: 0,
214 215
      title: new Text(StockStrings.of(context).title()),
      actions: <Widget>[
216
        new IconButton(
217
          icon: Icons.search,
Hixie's avatar
Hixie committed
218 219
          onPressed: _handleSearchBegin,
          tooltip: 'Search'
220
        ),
221 222
        new PopupMenuButton<_StockMenuItem>(
          onSelected: (_StockMenuItem value) { _handleStockMenu(context, value); },
Hans Muller's avatar
Hans Muller committed
223 224
          items: <PopupMenuItem<_StockMenuItem>>[
            new CheckedPopupMenuItem<_StockMenuItem>(
225
              value: _StockMenuItem.autorefresh,
226
              checked: _autorefresh,
227 228
              child: new Text('Autorefresh')
            ),
Hans Muller's avatar
Hans Muller committed
229
            new PopupMenuItem<_StockMenuItem>(
230 231 232
              value: _StockMenuItem.refresh,
              child: new Text('Refresh')
            ),
Hans Muller's avatar
Hans Muller committed
233
            new PopupMenuItem<_StockMenuItem>(
234 235 236
              value: _StockMenuItem.speedUp,
              child: new Text('Increase animation speed')
            ),
Hans Muller's avatar
Hans Muller committed
237
            new PopupMenuItem<_StockMenuItem>(
238 239 240 241
              value: _StockMenuItem.speedDown,
              child: new Text('Decrease animation speed')
            )
          ]
242
        )
243
      ],
244 245 246 247 248
      tabBar: new TabBar<StockHomeTab>(
        labels: <StockHomeTab, TabLabel>{
          StockHomeTab.market: new TabLabel(text: StockStrings.of(context).market()),
          StockHomeTab.portfolio: new TabLabel(text: StockStrings.of(context).portfolio())
        }
249
      )
250
    );
251 252
  }

Hixie's avatar
Hixie committed
253
  Iterable<Stock> _getStockList(Iterable<String> symbols) {
254 255
    return symbols.map((String symbol) => config.stocks[symbol])
        .where((Stock stock) => stock != null);
256 257 258
  }

  Iterable<Stock> _filterBySearchQuery(Iterable<Stock> stocks) {
259
    if (_searchQuery.text.isEmpty)
260
      return stocks;
261
    RegExp regexp = new RegExp(_searchQuery.text, caseSensitive: false);
Hixie's avatar
Hixie committed
262
    return stocks.where((Stock stock) => stock.symbol.contains(regexp));
263 264
  }

Hixie's avatar
Hixie committed
265 266 267 268 269
  void _buyStock(Stock stock, Key arrowKey) {
    setState(() {
      stock.percentChange = 100.0 * (1.0 / stock.lastSale);
      stock.lastSale += 1.0;
    });
270
    _scaffoldKey.currentState.showSnackBar(new SnackBar(
Hixie's avatar
Hixie committed
271
      content: new Text("Purchased ${stock.symbol} for ${stock.lastSale}"),
272 273 274 275 276 277
      action: new SnackBarAction(
        label: "BUY MORE",
        onPressed: () {
          _buyStock(stock, arrowKey);
        }
      )
Hixie's avatar
Hixie committed
278 279 280
    ));
  }

281
  Widget _buildStockList(BuildContext context, Iterable<Stock> stocks, StockHomeTab tab) {
282
    return new StockList(
283
      keySalt: tab,
284
      stocks: stocks.toList(),
Hixie's avatar
Hixie committed
285
      onAction: _buyStock,
Hixie's avatar
Hixie committed
286
      onOpen: (Stock stock, Key arrowKey) {
287
        Set<Key> mostValuableKeys = new HashSet<Key>();
Hixie's avatar
Hixie committed
288
        mostValuableKeys.add(arrowKey);
Hixie's avatar
Hixie committed
289
        Navigator.pushNamed(context, '/stock/${stock.symbol}', mostValuableKeys: mostValuableKeys);
290 291
      },
      onShow: (Stock stock, Key arrowKey) {
292
        _scaffoldKey.currentState.showBottomSheet((BuildContext context) => new StockSymbolBottomSheet(stock: stock));
293 294
      }
    );
295 296
  }

297 298 299 300 301 302 303
  Widget _buildStockTab(BuildContext context, StockHomeTab tab, List<String> stockSymbols) {
    return new Container(
      key: new ValueKey<StockHomeTab>(tab),
      child: _buildStockList(context, _filterBySearchQuery(_getStockList(stockSymbols)).toList(), tab)
    );
  }

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

306 307
  // TODO(abarth): Should we factor this into a SearchBar in the framework?
  Widget buildSearchBar() {
308 309
    return new AppBar(
      leading: new IconButton(
310
        icon: Icons.arrow_back,
Adam Barth's avatar
Adam Barth committed
311
        color: Theme.of(context).accentColor,
Hixie's avatar
Hixie committed
312 313
        onPressed: _handleSearchEnd,
        tooltip: 'Back'
314
      ),
315
      title: new Input(
316
        value: _searchQuery,
317
        autofocus: true,
318
        hintText: 'Search stocks',
319 320
        onChanged: _handleSearchQueryChanged
      ),
321
      backgroundColor: Theme.of(context).canvasColor
322 323 324
    );
  }

Hixie's avatar
Hixie committed
325
  void _handleCreateCompany() {
326
    showModalBottomSheet/*<Null>*/(
Adam Barth's avatar
Adam Barth committed
327
      context: context,
328
      builder: (BuildContext context) => new _CreateCompanySheet()
Matt Perry's avatar
Matt Perry committed
329
    );
330 331 332
  }

  Widget buildFloatingActionButton() {
333
    return new FloatingActionButton(
334
      tooltip: 'Create company',
335
      child: new Icon(icon: Icons.add),
336
      backgroundColor: Colors.redAccent[200],
Hixie's avatar
Hixie committed
337
      onPressed: _handleCreateCompany
338
    );
339 340
  }

341
  @override
342
  Widget build(BuildContext context) {
343 344
    return new TabBarSelection<StockHomeTab>(
      values: <StockHomeTab>[StockHomeTab.market, StockHomeTab.portfolio],
345 346
      child: new Scaffold(
        key: _scaffoldKey,
347
        appBar: _isSearching ? buildSearchBar() : buildAppBar(),
348 349
        floatingActionButton: buildFloatingActionButton(),
        drawer: _buildDrawer(context),
350
        body: new TabBarView<StockHomeTab>(
Adam Barth's avatar
Adam Barth committed
351 352 353 354
          children: <Widget>[
            _buildStockTab(context, StockHomeTab.market, config.symbols),
            _buildStockTab(context, StockHomeTab.portfolio, portfolioSymbols),
          ]
355
        )
356
      )
357
    );
358 359
  }
}
360

361
class _CreateCompanySheet extends StatefulWidget {
362
  @override
363 364 365 366 367 368 369 370 371 372 373 374
  _CreateCompanySheetState createState() => new _CreateCompanySheetState();
}

class _CreateCompanySheetState extends State<_CreateCompanySheet> {
  InputValue _companyName = InputValue.empty;

  void _handleCompanyNameChanged(InputValue value) {
    setState(() {
      _companyName = value;
    });
  }

375
  @override
376 377 378 379 380 381 382 383 384 385 386 387 388 389
  Widget build(BuildContext context) {
    // TODO(ianh): Fill this out.
    return new Column(
      children: <Widget>[
        new Input(
          autofocus: true,
          hintText: 'Company Name',
          value: _companyName,
          onChanged: _handleCompanyNameChanged
        ),
      ]
    );
  }
}