stock_home.dart 10.7 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'package:flutter/gestures.dart' show DragStartBehavior;
6
import 'package:flutter/material.dart';
7
import 'package:flutter/rendering.dart' show debugDumpRenderTree, debugDumpLayerTree, debugDumpSemanticsTree, DebugSemanticsDumpOrder;
8
import 'package:flutter/scheduler.dart' show timeDilation;
9 10

import 'i18n/stock_strings.dart';
11 12 13 14
import 'stock_data.dart';
import 'stock_list.dart';
import 'stock_symbol_viewer.dart';
import 'stock_types.dart';
15

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

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

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

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

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

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

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

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

85
  void _handleStockModeChange(StockMode? value) {
86 87
    if (widget.updater != null)
      widget.updater(widget.configuration.copyWith(stockMode: value));
88 89
  }

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

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

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

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

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

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

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

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

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

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

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

287
  AppBar buildSearchBar() {
288 289
    return AppBar(
      leading: BackButton(
290
        color: Theme.of(context).colorScheme.secondary,
291
      ),
292
      title: TextField(
293
        controller: _searchQuery,
294
        autofocus: true,
295 296 297
        decoration: const InputDecoration(
          hintText: 'Search stocks',
        ),
298
      ),
299
      backgroundColor: Theme.of(context).canvasColor,
300 301 302
    );
  }

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

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

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

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