Merge "SyncSlides: Initial app setup with some basic functionality implemented."
diff --git a/dart/Makefile b/dart/Makefile
index 2cde208..d8381d3 100644
--- a/dart/Makefile
+++ b/dart/Makefile
@@ -9,12 +9,15 @@
 dartfmt: packages
 	dartfmt --overwrite lib
 
-.PHONY: packages
-packages:
+packages: pubspec.yaml
+	pub get
+
+.PHONY: upgrade-packages
+upgrade-packages:
 	pub upgrade
 
 run: packages
-	pub run sky_tools build && pub run sky_tools run_mojo --mojo-path $(MOJO_DIR)/src --android --mojo-debug -- --enable-multiprocess --map-origin="https://syncslides.mojo.v.io/=$(PWD)" --args-for="https://syncslides.mojo.v.io/packages/syncbase/mojo_services/android/syncbase_server.mojo --v=1 --logtostderr=true --root-dir=/data/data/org.chromium.mojo.shell/app_home/syncbasedata" --no-config-file --free-host-ports
+	pub run sky_tools build && pub run sky_tools run_mojo --mojo-path $(MOJO_DIR)/src/mojo/devtools/common/mojo_run --android --mojo-debug -- --enable-multiprocess --map-origin="https://syncslides.mojo.v.io/=$(PWD)" --args-for="https://syncslides.mojo.v.io/packages/syncbase/mojo_services/android/syncbase_server.mojo --v=1 --logtostderr=true --root-dir=/data/data/org.chromium.mojo.shell/app_home/syncbasedata" --no-config-file --free-host-ports
 
 .PHONY: clean
 clean:
diff --git a/dart/README.md b/dart/README.md
index 085ad2c..ced3a3b 100644
--- a/dart/README.md
+++ b/dart/README.md
@@ -12,7 +12,10 @@
 
 Flutter depends on a relatively new version of the Dart SDK. Therefore, please ensure that you have installed the following version or greater:
 
-```Dart VM version: 1.13.0-dev.3.1 (Thu Sep 17 10:54:54 2015) on "linux_x64"```
+```
+Dart VM version: 1.13.0-dev.3.1 (Thu Sep 17 10:54:54 2015) on "linux_x64"
+```
+
 If you are unsure what version you are on, use `dart --version`.
 
 To install Dart, visit the [download page](https://www.dartlang.org/downloads/).
@@ -25,6 +28,6 @@
 # Running SyncSlides
 
 Connect your Android device via USB and ensure `Android debugging` is enabled, then execute:
-`
+```
 make run
-`
+```
diff --git a/dart/assets/images/sample_decks/baku/thumb.png b/dart/assets/images/sample_decks/baku/thumb.png
new file mode 100644
index 0000000..e8c8a09
--- /dev/null
+++ b/dart/assets/images/sample_decks/baku/thumb.png
Binary files differ
diff --git a/dart/assets/images/sample_decks/pitch/thumb.png b/dart/assets/images/sample_decks/pitch/thumb.png
new file mode 100644
index 0000000..85b9ff8
--- /dev/null
+++ b/dart/assets/images/sample_decks/pitch/thumb.png
Binary files differ
diff --git a/dart/assets/images/sample_decks/vanadium/1.jpg b/dart/assets/images/sample_decks/vanadium/1.jpg
new file mode 100644
index 0000000..5659a08
--- /dev/null
+++ b/dart/assets/images/sample_decks/vanadium/1.jpg
Binary files differ
diff --git a/dart/assets/images/sample_decks/vanadium/2.jpg b/dart/assets/images/sample_decks/vanadium/2.jpg
new file mode 100644
index 0000000..cea86f7
--- /dev/null
+++ b/dart/assets/images/sample_decks/vanadium/2.jpg
Binary files differ
diff --git a/dart/assets/images/sample_decks/vanadium/3.jpg b/dart/assets/images/sample_decks/vanadium/3.jpg
new file mode 100644
index 0000000..1a3c1f4
--- /dev/null
+++ b/dart/assets/images/sample_decks/vanadium/3.jpg
Binary files differ
diff --git a/dart/assets/images/sample_decks/vanadium/4.jpg b/dart/assets/images/sample_decks/vanadium/4.jpg
new file mode 100644
index 0000000..fb0e8cf
--- /dev/null
+++ b/dart/assets/images/sample_decks/vanadium/4.jpg
Binary files differ
diff --git a/dart/assets/images/sample_decks/vanadium/5.jpg b/dart/assets/images/sample_decks/vanadium/5.jpg
new file mode 100644
index 0000000..85368b9
--- /dev/null
+++ b/dart/assets/images/sample_decks/vanadium/5.jpg
Binary files differ
diff --git a/dart/assets/images/sample_decks/vanadium/6.jpg b/dart/assets/images/sample_decks/vanadium/6.jpg
new file mode 100644
index 0000000..0f99159
--- /dev/null
+++ b/dart/assets/images/sample_decks/vanadium/6.jpg
Binary files differ
diff --git a/dart/assets/images/sample_decks/vanadium/thumb.png b/dart/assets/images/sample_decks/vanadium/thumb.png
new file mode 100644
index 0000000..11ecb19
--- /dev/null
+++ b/dart/assets/images/sample_decks/vanadium/thumb.png
Binary files differ
diff --git a/dart/flutter.yaml b/dart/flutter.yaml
new file mode 100644
index 0000000..13c17d3
--- /dev/null
+++ b/dart/flutter.yaml
@@ -0,0 +1,3 @@
+name: syncslides
+material-design-icons:
+  - name: navigation/arrow_back
diff --git a/dart/lib/components/deckgrid.dart b/dart/lib/components/deckgrid.dart
new file mode 100644
index 0000000..5ed43ab
--- /dev/null
+++ b/dart/lib/components/deckgrid.dart
@@ -0,0 +1,96 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
+
+import '../models/all.dart' as model;
+import '../stores/store.dart';
+import '../styles/common.dart' as style;
+
+import 'slidelist.dart';
+
+// DeckGridPage is the full page view of the list of decks.
+class DeckGridPage extends StatelessComponent {
+  Widget build(BuildContext context) {
+    return new Scaffold(
+        toolBar: new ToolBar(center: new Text('SyncSlides')),
+        body: new Material(child: new DeckGrid()));
+  }
+}
+
+// DeckGrid is scrollable grid view of decks.
+class DeckGrid extends StatefulComponent {
+  _DeckGridState createState() => new _DeckGridState();
+}
+
+class _DeckGridState extends State<DeckGrid> {
+  Store _store = new Store.singleton();
+  List<model.Deck> _decks = new List<model.Deck>();
+  StreamSubscription _onChangeSubscription;
+
+  void updateDecks(List<model.Deck> decks) {
+    setState(() {
+      _decks = decks;
+    });
+  }
+
+  @override
+  void initState() {
+    _store.getAllDecks().then(updateDecks);
+    // Update the state whenever store tells us decks have changed.
+    _onChangeSubscription = _store.onDecksChange.listen(updateDecks);
+    super.initState();
+  }
+
+  @override
+  void dispose() {
+    // Stop listening to updates from store when component is disposed.
+    _onChangeSubscription.cancel();
+    super.dispose();
+  }
+
+  Widget build(BuildContext context) {
+    var deckBoxes = _decks.map((deck) => _buildDeckBox(context, deck)).toList();
+    var grid = new Grid(deckBoxes, maxChildExtent: style.Size.thumbnailWidth);
+    return new ScrollableViewport(child: grid);
+  }
+}
+
+// TODO(aghassemi): Is this approach okay? Check with Flutter team.
+// Building RawImage is expensive, so we cache.
+// Expando is a weak map so this does not effect GC.
+Expando<Widget> weakDeckItemCache = new Expando<Widget>();
+Widget _buildDeckBox(BuildContext context, model.Deck deckData) {
+  var cachedWidget = weakDeckItemCache[deckData];
+  if (cachedWidget != null) {
+    return cachedWidget;
+  }
+
+  var thumbnail;
+  if (deckData.thumbnail != null) {
+    thumbnail = new RawImage(bytes: new Uint8List.fromList(deckData.thumbnail));
+  } else {
+    // TODO(aghassemi): Replace with a proper default thumbnail.
+    thumbnail = new Text('No Thumbnail Image');
+  }
+
+  var title = new Text(deckData.name, style: style.Text.titleStyle);
+  var titleAndActions =
+      new Container(child: title, padding: style.Spacing.normalPadding);
+
+  var card = new Container(
+      child: new Card(child: new Block([thumbnail, titleAndActions])),
+      margin: style.Spacing.normalMargin);
+
+  var gridItem = new InkWell(child: card, onTap: () {
+    Navigator.of(context).push(new PageRoute(
+        builder: (context) => new SlideListPage(deckData.key, deckData.name)));
+  });
+
+  weakSlideCache[deckData] = gridItem;
+  return gridItem;
+}
diff --git a/dart/lib/components/slidelist.dart b/dart/lib/components/slidelist.dart
new file mode 100644
index 0000000..6cfbd68
--- /dev/null
+++ b/dart/lib/components/slidelist.dart
@@ -0,0 +1,107 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+import 'package:flutter/widgets.dart';
+import 'package:flutter/material.dart';
+
+import '../models/all.dart' as model;
+import '../stores/store.dart';
+import '../styles/common.dart' as style;
+
+import '../utils/keyvalue.dart';
+
+// SlideListPage is the full page view of the list of slides for a deck.
+class SlideListPage extends StatelessComponent {
+  String _deckId;
+  String _title;
+
+  SlideListPage(this._deckId, this._title);
+  Widget build(BuildContext context) {
+    return new Scaffold(
+        toolBar: new ToolBar(
+            left: new IconButton(
+                icon: 'navigation/arrow_back',
+                onPressed: () => Navigator.of(context).pop()),
+            center: new Text(_title)),
+        body: new Material(child: new SlideList(_deckId)));
+  }
+}
+
+// SlideList is scrollable list view of slides for a deck.
+class SlideList extends StatefulComponent {
+  final String deckId;
+  SlideList(this.deckId);
+
+  _SlideListState createState() => new _SlideListState();
+}
+
+class _SlideListState extends State<SlideList> {
+  _SlideListState();
+  Store _store = new Store.singleton();
+  List<model.Slide> _slides = new List<model.Slide>();
+
+  void updateSlides(List<model.Slide> slides) {
+    setState(() {
+      _slides = slides;
+    });
+  }
+
+  void initState() {
+    super.initState();
+    _store.getAllSlides(config.deckId).then(updateSlides);
+  }
+
+  Widget build(BuildContext context) {
+    // Create a list of <SlideNumber, Slide> pairs.
+    List<KeyValue<String, model.Slide>> slidesWithPosition = [];
+    for (var i = 0; i < _slides.length; i++) {
+      slidesWithPosition.add(new KeyValue(i.toString(), _slides[i]));
+    }
+    return new ScrollableList(
+        itemExtent: style.Size.listHeight,
+        items: slidesWithPosition,
+        itemBuilder: (context, kv) => _buildSlide(context, kv.key, kv.value));
+  }
+}
+
+// TODO(aghassemi): Is this approach okay? Check with Flutter team.
+// Builder gets called a lot by the ScrollableList and building RawImage
+// is expensive so we cache.
+// Expando is a weak map so this does not effect GC.
+Expando<Widget> weakSlideCache = new Expando<Widget>();
+Widget _buildSlide(BuildContext context, String key, model.Slide slideData) {
+  var cachedWidget = weakSlideCache[slideData];
+  if (cachedWidget != null) {
+    return cachedWidget;
+  }
+
+  var thumbnail;
+  if (slideData.image != null) {
+    thumbnail = new RawImage(
+        height: style.Size.listHeight,
+        bytes: new Uint8List.fromList(slideData.image),
+        fit: ImageFit.cover);
+  } else {
+    // TODO(aghassemi): Replace with a proper default thumbnail.
+    thumbnail = new Text('No Slide Image');
+  }
+  thumbnail = new Flexible(child: thumbnail);
+
+  var title = new Text('Slide $key', style: style.Text.subTitleStyle);
+  var notes = new Text(
+      'This is the teaser slide. It should be memorable and descriptive');
+  var titleAndNotes = new Flexible(
+      child: new Container(
+          child: new Column([title, notes], alignItems: FlexAlignItems.start),
+          padding: style.Spacing.normalPadding));
+
+  var card = new Container(
+      child: new Card(child: new Row([thumbnail, titleAndNotes])),
+      margin: style.Spacing.listItemMargin);
+
+  var listItem = new InkWell(key: new Key(key), child: card);
+
+  weakSlideCache[slideData] = listItem;
+  return listItem;
+}
diff --git a/dart/lib/config.dart b/dart/lib/config.dart
new file mode 100644
index 0000000..fe5bca2
--- /dev/null
+++ b/dart/lib/config.dart
@@ -0,0 +1,7 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// TODO(aghassemi): Make these configurable from command line and/or UI.
+bool SyncbaseEnabled = true;
+bool DemoEnabled = true;
diff --git a/dart/lib/loaders/demo_loader.dart b/dart/lib/loaders/demo_loader.dart
new file mode 100644
index 0000000..ad0a089
--- /dev/null
+++ b/dart/lib/loaders/demo_loader.dart
@@ -0,0 +1,89 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'dart:math';
+import 'dart:typed_data';
+
+import 'package:flutter/services.dart' as services;
+
+import '../models/all.dart' as model;
+import '../stores/store.dart';
+
+import 'loader.dart';
+
+// DemoLoader loads some sample decks and slides and randomly adds/removes
+// decks based on a timer.
+class DemoLoader implements Loader {
+  final Store _store;
+  final Random _rand;
+
+  DemoLoader()
+      : _store = new Store.singleton(),
+        _rand = new Random();
+
+  static const int numDeckSets = 2;
+  Stream<model.Deck> _getSampleDecks() async* {
+    for (var i = 1; i <= numDeckSets; i++) {
+      yield new model.Deck('pitch$i', 'Pitch Deck #$i',
+          await _getRawBytes('assets/images/sample_decks/pitch/thumb.png'));
+      yield new model.Deck('baku$i', 'Baku Discovery Discussion #$i',
+          await _getRawBytes('assets/images/sample_decks/baku/thumb.png'));
+      yield new model.Deck('vanadium$i', 'Vanadium #$i',
+          await _getRawBytes('assets/images/sample_decks/vanadium/thumb.png'));
+    }
+  }
+
+  Stream<model.Slide> _getSampleSlides() async* {
+    // TODO(aghassemi): We need different slides for different decks.
+    // For now use Vanadium slides for all.
+    for (var i = 1; i <= 6; i++) {
+      yield new model.Slide(
+          await _getRawBytes('assets/images/sample_decks/vanadium/$i.jpg'));
+    }
+  }
+
+  Future loadDecks() async {
+    // Add some initial decks.
+    await for (var deck in _getSampleDecks()) {
+      await _addDeck(deck);
+    }
+
+    // Periodically add or remove random decks.
+    new Timer.periodic(new Duration(seconds: 2), (_) async {
+      var decks = await _store.getAllDecks();
+      var deckKeys = decks.map((d) => d.key);
+      var removeDeck = _rand.nextBool();
+
+      if (removeDeck && decks.length > 0) {
+        _store.removeDeck(decks[_rand.nextInt(decks.length)].key);
+      } else {
+        await for (var deck in _getSampleDecks()) {
+          if (!deckKeys.contains(deck.key)) {
+            await _addDeck(deck);
+            break;
+          }
+        }
+      }
+    });
+  }
+
+  Future _addDeck(model.Deck deck) async {
+    await _store.addDeck(deck);
+    List<model.Slide> slides = await _getSampleSlides().toList();
+    await _store.setSlides(deck.key, slides);
+  }
+
+  Map<String, Uint8List> _assetCache = new Map<String, Uint8List>();
+  Future<Uint8List> _getRawBytes(String url) async {
+    if (_assetCache.containsKey(url)) {
+      return _assetCache[url];
+    }
+    services.Response response = await services.fetchBody(url);
+    var bytes = new Uint8List.fromList(response.body.buffer.asUint8List());
+    _assetCache[url] = bytes;
+    return bytes;
+  }
+}
diff --git a/dart/lib/loaders/loader.dart b/dart/lib/loaders/loader.dart
new file mode 100644
index 0000000..ec97da1
--- /dev/null
+++ b/dart/lib/loaders/loader.dart
@@ -0,0 +1,21 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'loader_factory.dart' as loaderFactory;
+
+// Loader is responsible for importing existing decks and slides into the store.
+abstract class Loader {
+  static Loader _singletonLoader;
+
+  factory Loader.singleton() {
+    if (_singletonLoader == null) {
+      _singletonLoader = loaderFactory.create();
+    }
+    return _singletonLoader;
+  }
+
+  Future loadDecks();
+}
diff --git a/dart/lib/loaders/loader_factory.dart b/dart/lib/loaders/loader_factory.dart
new file mode 100644
index 0000000..7b006f1
--- /dev/null
+++ b/dart/lib/loaders/loader_factory.dart
@@ -0,0 +1,18 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+import '../config.dart' as config;
+
+import 'loader.dart';
+import 'demo_loader.dart';
+import 'sdcard_loader.dart';
+
+// Factory method to create a concrete loader instance.
+Loader create() {
+  if (config.DemoEnabled) {
+    return new DemoLoader();
+  } else {
+    return new SdCardLoader();
+  }
+}
diff --git a/dart/lib/loaders/sdcard_loader.dart b/dart/lib/loaders/sdcard_loader.dart
new file mode 100644
index 0000000..72071c2
--- /dev/null
+++ b/dart/lib/loaders/sdcard_loader.dart
@@ -0,0 +1,13 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'loader.dart';
+
+class SdCardLoader implements Loader {
+  Future loadDecks() {
+    throw new UnimplementedError();
+  }
+}
diff --git a/dart/lib/main.dart b/dart/lib/main.dart
index 72890ac..8cc1ab6 100644
--- a/dart/lib/main.dart
+++ b/dart/lib/main.dart
@@ -2,139 +2,16 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-import 'dart:async';
-import 'dart:math' show Random;
-import 'dart:convert' show UTF8;
-
 import 'package:flutter/material.dart';
 
-import 'package:flutter/services.dart' show embedder;
-import 'package:syncbase/syncbase_client.dart' as sb;
+import 'components/deckgrid.dart';
+import 'loaders/loader.dart';
 
-// TODO(aghassemi) Temporary main.
 void main() {
+  // Start loading data.
+  new Loader.singleton().loadDecks();
+
   runApp(new MaterialApp(
-      title: "Flutter & Syncbase Demo",
-      routes: {'/': (RouteArguments args) => new FlutterSyncbaseDemo()}));
-}
-
-class FlutterSyncbaseDemoState extends State<FlutterSyncbaseDemo> {
-  List<String> activityLog = [];
-
-  void addActivityLogItem(String item) {
-    setState(() {
-      activityLog.add(item);
-    });
-  }
-
-  Widget build(BuildContext context) {
-    return new Scaffold(
-        toolBar: new ToolBar(center: new Text("Flutter & Syncbase Demo")),
-        body: new Material(child: new Text(activityLog.join("\n"))));
-  }
-}
-
-class FlutterSyncbaseDemo extends StatefulComponent {
-  FlutterSyncbaseDemoState _state = new FlutterSyncbaseDemoState();
-
-  FlutterSyncbaseDemo() {
-    initSyncbase(_state);
-  }
-
-  FlutterSyncbaseDemoState createState() {
-    return _state;
-  }
-}
-
-bool initialized = false;
-initSyncbase(FlutterSyncbaseDemoState state) async {
-  if (initialized) {
-    return;
-  }
-
-  initialized = true;
-  sb.SyncbaseClient c = new sb.SyncbaseClient(embedder.connectToService,
-      'https://syncslides.mojo.v.io/packages/syncbase/mojo_services/android/syncbase_server.mojo');
-
-  sb.SyncbaseApp sbApp = await createApp(c, 'testapp');
-  sb.SyncbaseNoSqlDatabase sbDb = await createDb(sbApp, 'testdb');
-  sb.SyncbaseTable sbTable = await createTable(sbDb, 'testtable');
-
-  startWatch(sbDb, sbTable, state);
-  startPuts(sbTable, state);
-
-  // Wait forever.
-  await new Completer().future;
-
-  // Looks like forever came and went.  Might as well clean up after
-  // ourselves...
-  await c.close();
-}
-
-startWatch(db, table, state) async {
-  var s = db.watch(table.name, '', await db.getResumeMarker());
-  await for (var change in s) {
-    var activity =
-        'GOT CHANGE: ${change.rowKey} - ${UTF8.decode(change.valueBytes)}';
-    state.addActivityLogItem(activity);
-    print(activity);
-  }
-}
-
-var r = new Random();
-
-startPuts(table, state) async {
-  var key = r.nextInt(100000000);
-  var val = r.nextInt(100000000);
-
-  var row = table.row('k-$key');
-  var activity = 'PUTTING k-$key';
-  state.addActivityLogItem(activity);
-  print(activity);
-  await row.put(UTF8.encode('v-$val'));
-
-  await new Future.delayed(new Duration(seconds: 2));
-  startPuts(table, state);
-}
-
-String openPermsJson =
-    '{"Admin":{"In":["..."]},"Write":{"In":["..."]},"Read":{"In":["..."]},"Resolve":{"In":["..."]},"Debug":{"In":["..."]}}';
-sb.Perms openPerms = sb.SyncbaseClient.perms(openPermsJson);
-
-Future<sb.SyncbaseApp> createApp(sb.SyncbaseClient c, String name) async {
-  var app = c.app(name);
-  var exists = await app.exists();
-  if (exists) {
-    print('app exists, rolling with it');
-    return app;
-  }
-  print('app does not exist, creating it');
-  await app.create(openPerms);
-  return app;
-}
-
-Future<sb.SyncbaseNoSqlDatabase> createDb(
-    sb.SyncbaseApp app, String name) async {
-  var db = app.noSqlDatabase(name);
-  var exists = await db.exists();
-  if (exists) {
-    print('db exists, rolling with it');
-    return db;
-  }
-  print('db does not exist, creating it');
-  await db.create(openPerms);
-  return db;
-}
-
-Future<sb.SyncbaseTable> createTable(
-    sb.SyncbaseNoSqlDatabase db, String name) async {
-  var table = db.table(name);
-  var exists = await table.exists();
-  if (exists) {
-    print('table exists, rolling with it');
-    return table;
-  }
-  print('table does not exist, creating it');
-  await table.create(openPerms);
-  return table;
+      title: 'SyncSlides',
+      routes: {'/': (RouteArguments args) => new DeckGridPage()}));
 }
diff --git a/dart/lib/models/all.dart b/dart/lib/models/all.dart
new file mode 100644
index 0000000..61a7d0f
--- /dev/null
+++ b/dart/lib/models/all.dart
@@ -0,0 +1,6 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+export 'deck.dart';
+export 'slide.dart';
diff --git a/dart/lib/models/deck.dart b/dart/lib/models/deck.dart
new file mode 100644
index 0000000..6e389f1
--- /dev/null
+++ b/dart/lib/models/deck.dart
@@ -0,0 +1,34 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+import 'dart:convert';
+
+// Deck represents a deck of slides.
+class Deck {
+  String _key;
+  String get key => _key;
+
+  String _name;
+  String get name => _name;
+
+  List<int> _thumbnail;
+  List<int> get thumbnail => _thumbnail;
+
+  Deck(this._key, this._name, this._thumbnail) {}
+
+  Deck.fromJson(String key, String json) {
+    Map map = JSON.decode(json);
+    _key = key;
+    _name = map['name'];
+    _thumbnail = map['thumbnail'];
+  }
+
+  String toJson() {
+    // NOTE(aghassemi): We never serialize the key with the object.
+    Map map = new Map();
+    map['name'] = name;
+    map['thumbnail'] = thumbnail;
+    return JSON.encode(map);
+  }
+}
diff --git a/dart/lib/models/slide.dart b/dart/lib/models/slide.dart
new file mode 100644
index 0000000..ec24128
--- /dev/null
+++ b/dart/lib/models/slide.dart
@@ -0,0 +1,24 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+import 'dart:convert';
+
+// Slide represents an independent slide without ties to a specific deck.
+class Slide {
+  List<int> _image;
+  List<int> get image => _image;
+
+  Slide(this._image) {}
+
+  Slide.fromJson(String json) {
+    Map map = JSON.decode(json);
+    _image = map['image'];
+  }
+
+  String toJson() {
+    Map map = new Map();
+    map['image'] = image;
+    return JSON.encode(map);
+  }
+}
diff --git a/dart/lib/stores/keyutil.dart b/dart/lib/stores/keyutil.dart
new file mode 100644
index 0000000..d2cd08b
--- /dev/null
+++ b/dart/lib/stores/keyutil.dart
@@ -0,0 +1,18 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Constructs a slide key.
+String getSlideKey(String deckId, int slideIndex) {
+  return '$deckId/slides/$slideIndex';
+}
+
+// Constructs prefix key for a deck.
+String getDeckKeyPrefix(String deckKey) {
+  return deckKey + '/';
+}
+
+// Returns true if a key is for a deck.
+bool isDeckKey(String key) {
+  return !key.contains('/');
+}
diff --git a/dart/lib/stores/memory_store.dart b/dart/lib/stores/memory_store.dart
new file mode 100644
index 0000000..5c092c0
--- /dev/null
+++ b/dart/lib/stores/memory_store.dart
@@ -0,0 +1,71 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import '../models/all.dart' as model;
+
+import 'keyutil.dart' as keyutil;
+import 'store.dart';
+
+// A memory-based implementation of Store.
+class MemoryStore implements Store {
+  StreamController _onDecksChangeController;
+  Map<String, String> _decksMap;
+  Map<String, String> _slidesMap;
+
+  MemoryStore()
+      : _onDecksChangeController = new StreamController.broadcast(),
+        _decksMap = new Map(),
+        _slidesMap = new Map();
+
+  Future<List<model.Deck>> getAllDecks() async {
+    var decks = [];
+    _decksMap.forEach((String key, String value) {
+      decks.add(new model.Deck.fromJson(key, value));
+    });
+
+    return decks;
+  }
+
+  Future addDeck(model.Deck deck) async {
+    var json = deck.toJson();
+    _decksMap[deck.key] = json;
+    getAllDecks().then(_triggerDecksChangeEvent);
+  }
+
+  Future removeDeck(String deckKey) async {
+    _decksMap.remove(deckKey);
+    _slidesMap.keys
+        .where((slideKey) =>
+            slideKey.startsWith(keyutil.getDeckKeyPrefix(deckKey)))
+        .toList()
+        .forEach(_slidesMap.remove);
+    getAllDecks().then(_triggerDecksChangeEvent);
+  }
+
+  Stream<List<model.Deck>> get onDecksChange => _onDecksChangeController.stream;
+
+  Future<List<model.Slide>> getAllSlides(String deckKey) async {
+    var slides = [];
+    _slidesMap.keys
+        .where((slideKey) =>
+            slideKey.startsWith(keyutil.getDeckKeyPrefix(deckKey)))
+        .forEach((String key) {
+      slides.add(new model.Slide.fromJson(_slidesMap[key]));
+    });
+    return slides;
+  }
+
+  Future setSlides(String deckKey, List<model.Slide> slides) async {
+    List<String> jsonSlides = slides.map((slide) => slide.toJson()).toList();
+    for (int i = 0; i < jsonSlides.length; i++) {
+      _slidesMap[keyutil.getSlideKey(deckKey, i)] = jsonSlides[i];
+    }
+  }
+
+  _triggerDecksChangeEvent(List<model.Deck> decks) {
+    _onDecksChangeController.add(decks);
+  }
+}
diff --git a/dart/lib/stores/store.dart b/dart/lib/stores/store.dart
new file mode 100644
index 0000000..4dec8b5
--- /dev/null
+++ b/dart/lib/stores/store.dart
@@ -0,0 +1,46 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import '../models/all.dart' as model;
+
+import 'store_factory.dart' as storeFactory;
+
+// Provides APIs for reading and writing app-related data.
+abstract class Store {
+  static Store _singletonStore;
+
+  factory Store.singleton() {
+    if (_singletonStore == null) {
+      _singletonStore = storeFactory.create();
+    }
+    return _singletonStore;
+  }
+
+  //////////////////////////////////////
+  /// Decks
+
+  // Returns all the existing decks.
+  Future<List<model.Deck>> getAllDecks();
+
+  // Adds a new deck.
+  Future addDeck(model.Deck deck);
+
+  // Removed a deck given its key.
+  Future removeDeck(String key);
+
+  // Event that fires when deck are added or removed.
+  // The up-to-date list of decks with be sent to listeners.
+  Stream<List<model.Deck>> get onDecksChange;
+
+  //////////////////////////////////////
+  /// Slides
+
+  // Returns the list of all slides for a deck.
+  Future<List<model.Slide>> getAllSlides(String deckKey);
+
+  // Sets the slides for a deck.
+  Future setSlides(String deckKey, List<model.Slide> slides);
+}
diff --git a/dart/lib/stores/store_factory.dart b/dart/lib/stores/store_factory.dart
new file mode 100644
index 0000000..c8cc53e
--- /dev/null
+++ b/dart/lib/stores/store_factory.dart
@@ -0,0 +1,18 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+import '../config.dart' as config;
+
+import 'store.dart';
+import 'memory_store.dart';
+import 'syncbase_store.dart';
+
+// Factory method to create a concrete store instance.
+Store create() {
+  if (config.SyncbaseEnabled) {
+    return new SyncbaseStore();
+  } else {
+    return new MemoryStore();
+  }
+}
diff --git a/dart/lib/stores/syncbase_store.dart b/dart/lib/stores/syncbase_store.dart
new file mode 100644
index 0000000..e2e6333
--- /dev/null
+++ b/dart/lib/stores/syncbase_store.dart
@@ -0,0 +1,123 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+
+import '../models/all.dart' as model;
+import '../syncbase/client.dart' as sb;
+
+import 'keyutil.dart' as keyutil;
+import 'store.dart';
+
+const String decksTableName = 'Decks';
+
+// Implementation of  using Syncbase (http://v.io/syncbase) storage system.
+class SyncbaseStore implements Store {
+  StreamController _onDecksChangeController;
+  SyncbaseStore() {
+    _onDecksChangeController = new StreamController.broadcast();
+    _onDecksChangeController.onListen = () {
+      sb.getDatabase().then(_startDecksWatch);
+    };
+  }
+
+  Future<List<model.Deck>> getAllDecks() async {
+    // Key schema is:
+    // <deckId> --> Deck
+    // <deckId>/slides/1 --> Slide
+    // So we scan for keys that don't have /
+    // Ideally this would become a query based on Type when there is VOM/VDL
+    // support in Dart and we store typed objects instead of JSON bytes.
+    sb.SyncbaseNoSqlDatabase sbDb = await sb.getDatabase();
+    String query = 'SELECT k, v FROM $decksTableName WHERE k NOT LIKE "%/%"';
+    Stream<sb.Result> results = sbDb.exec(query);
+    // NOTE(aghassemi): First row is always the name of the columns, so we skip(1).
+    return results.skip(1).map((result) => _toDeck(result.values)).toList();
+  }
+
+  Future addDeck(model.Deck deck) async {
+    sb.SyncbaseTable tb = await _getDecksTable();
+    tb.put(deck.key, UTF8.encode(deck.toJson()));
+  }
+
+  Future removeDeck(String deckKey) async {
+    sb.SyncbaseTable tb = await _getDecksTable();
+    // Delete deck and all of its slides.
+    tb.deleteRange(new sb.RowRange.prefix(deckKey));
+  }
+
+  Stream<List<model.Deck>> get onDecksChange => _onDecksChangeController.stream;
+
+  Future<List<model.Slide>> getAllSlides(String deckKey) async {
+    // Key schema is:
+    // <deckId> --> Deck
+    // <deckId>/slides/1 --> Slide
+    // So we scan for keys that start with $deckKey/
+    // Ideally this would have been a query based on Type but that is not supported yet.
+    sb.SyncbaseNoSqlDatabase sbDb = await sb.getDatabase();
+    String prefix = keyutil.getDeckKeyPrefix(deckKey);
+    String query = 'SELECT k, v FROM $decksTableName WHERE k LIKE "$prefix%"';
+    Stream results = sbDb.exec(query);
+    return results.skip(1).map((result) => _toSlide(result.values)).toList();
+  }
+
+  Future setSlides(String deckKey, List<model.Slide> slides) async {
+    sb.SyncbaseTable tb = await _getDecksTable();
+
+    for (var i = 0; i < slides.length; i++) {
+      var slide = slides[i];
+      // TODO(aghassemi): Use batching when support is added.
+      await tb.put(
+          keyutil.getSlideKey(deckKey, i), UTF8.encode(slide.toJson()));
+    }
+  }
+
+  Future<sb.SyncbaseTable> _getDecksTable() async {
+    sb.SyncbaseNoSqlDatabase sbDb = await sb.getDatabase();
+    sb.SyncbaseTable tb = sbDb.table(decksTableName);
+    if (await tb.exists()) {
+      return tb;
+    }
+    await tb.create(sb.createOpenPermissions());
+    return tb;
+  }
+
+  Future _startDecksWatch(sb.SyncbaseNoSqlDatabase sbDb) async {
+    var resumeMarker = await sbDb.getResumeMarker();
+    var stream = sbDb.watch(decksTableName, '', resumeMarker);
+
+    var streamListener = stream.listen((sb.WatchChange change) async {
+      if (keyutil.isDeckKey(change.rowKey)) {
+        // TODO(aghassemi): Maybe manipulate an in-memory list based on watch
+        // changes instead of getting the decks again from Syncbase.
+        var decks = await getAllDecks();
+        _onDecksChangeController.add(decks);
+      }
+    });
+
+    // TODO(aghassemi): Currently we can not cancel a watch, only pause it.
+    // Since watch stream supports blocking flow control, it is not a big deal
+    // but ideally we can fully cancel a watch instead of enduing up with many
+    // paused watches.
+    // Also this issue becomes irrelevant if we do the TODO above regarding
+    // keeping and manipulating an in-memory list based on watch.
+    // https://github.com/vanadium/issues/issues/833
+    _onDecksChangeController.onCancel = () => streamListener.pause();
+  }
+
+  model.Deck _toDeck(List<List<int>> row) {
+    // TODO(aghassemi): Keys return from queries seems to have double quotes
+    // around them.
+    // See https://github.com/vanadium/issues/issues/860
+    var key = UTF8.decode(row[0]).replaceAll('"', '');
+    var value = UTF8.decode(row[1]);
+    return new model.Deck.fromJson(key, value);
+  }
+
+  model.Slide _toSlide(List<List<int>> row) {
+    var value = UTF8.decode(row[1]);
+    return new model.Slide.fromJson(value);
+  }
+}
diff --git a/dart/lib/styles/common.dart b/dart/lib/styles/common.dart
new file mode 100644
index 0000000..41c0a7e
--- /dev/null
+++ b/dart/lib/styles/common.dart
@@ -0,0 +1,23 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+import 'package:flutter/widgets.dart';
+
+class Text {
+  static final Color secondaryTextColor = new Color.fromARGB(70, 0, 0, 0);
+  static final TextStyle titleStyle = new TextStyle(fontSize: 18.0);
+  static final TextStyle subTitleStyle =
+      new TextStyle(fontSize: 12.0, color: secondaryTextColor);
+}
+
+class Size {
+  static const double thumbnailWidth = 250.0;
+  static const double listHeight = 150.0;
+}
+
+class Spacing {
+  static final EdgeDims normalPadding = new EdgeDims.all(10.0);
+  static final EdgeDims normalMargin = new EdgeDims.all(2.0);
+  static final EdgeDims listItemMargin = new EdgeDims.TRBL(3.0, 6.0, 0.0, 6.0);
+}
diff --git a/dart/lib/syncbase/client.dart b/dart/lib/syncbase/client.dart
new file mode 100644
index 0000000..b64ad7f
--- /dev/null
+++ b/dart/lib/syncbase/client.dart
@@ -0,0 +1,56 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:flutter/services.dart' show shell;
+import 'package:syncbase/syncbase_client.dart';
+
+export 'package:syncbase/syncbase_client.dart';
+
+const String syncbaseMojoUrl =
+    'https://syncslides.mojo.v.io/packages/syncbase/mojo_services/android/syncbase_server.mojo';
+const appName = 'syncslides';
+const dbName = 'syncslides';
+
+SyncbaseNoSqlDatabase _db;
+
+// Returns the database handle for the SyncSlides app.
+Future<SyncbaseNoSqlDatabase> getDatabase() async {
+  if (_db != null) {
+    return _db;
+  }
+
+  // Initialize Syncbase app and database.
+  SyncbaseClient sbClient =
+      new SyncbaseClient(shell.connectToService, syncbaseMojoUrl);
+  SyncbaseApp sbApp = await _createApp(sbClient);
+  _db = await _createDb(sbApp);
+
+  return _db;
+}
+
+Future<SyncbaseApp> _createApp(SyncbaseClient sbClient) async {
+  var app = sbClient.app(appName);
+  if (await app.exists()) {
+    return app;
+  }
+  await app.create(createOpenPerms());
+  return app;
+}
+
+Future<SyncbaseNoSqlDatabase> _createDb(SyncbaseApp app) async {
+  var db = app.noSqlDatabase(dbName);
+  if (await db.exists()) {
+    return db;
+  }
+  await db.create(createOpenPerms());
+  return db;
+}
+
+const String openPermsJson =
+    '{"Admin":{"In":["..."]},"Write":{"In":["..."]},"Read":{"In":["..."]},"Resolve":{"In":["..."]},"Debug":{"In":["..."]}}"';
+Perms createOpenPerms() {
+  return SyncbaseClient.perms(openPermsJson);
+}
diff --git a/dart/lib/utils/keyvalue.dart b/dart/lib/utils/keyvalue.dart
new file mode 100644
index 0000000..8fa83d7
--- /dev/null
+++ b/dart/lib/utils/keyvalue.dart
@@ -0,0 +1,11 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// KeyValue presents a generic pair of key and value objects.
+class KeyValue<T1, T2> {
+  T1 key;
+  T2 value;
+
+  KeyValue(this.key, this.value);
+}
diff --git a/dart/pubspec.lock b/dart/pubspec.lock
index 5790cde..7eaa90a 100644
--- a/dart/pubspec.lock
+++ b/dart/pubspec.lock
@@ -4,7 +4,7 @@
   analyzer:
     description: analyzer
     source: hosted
-    version: "0.26.1+16"
+    version: "0.26.2+1"
   archive:
     description: archive
     source: hosted
@@ -52,7 +52,7 @@
   convert:
     description: convert
     source: hosted
-    version: "1.0.0"
+    version: "1.0.1"
   crypto:
     description: crypto
     source: hosted
@@ -72,11 +72,11 @@
   flutter:
     description: flutter
     source: hosted
-    version: "0.0.13"
+    version: "0.0.17"
   flx:
     description: flx
     source: hosted
-    version: "0.0.1"
+    version: "0.0.9"
   glob:
     description: glob
     source: hosted
@@ -168,7 +168,7 @@
   pub_semver:
     description: pub_semver
     source: hosted
-    version: "1.2.2"
+    version: "1.2.3"
   quiver:
     description: quiver
     source: hosted
@@ -196,15 +196,15 @@
   sky_engine:
     description: sky_engine
     source: hosted
-    version: "0.0.43"
+    version: "0.0.48"
   sky_services:
     description: sky_services
     source: hosted
-    version: "0.0.43"
+    version: "0.0.48"
   sky_tools:
     description: sky_tools
     source: hosted
-    version: "0.0.27"
+    version: "0.0.37"
   source_map_stack_trace:
     description: source_map_stack_trace
     source: hosted
@@ -220,7 +220,7 @@
   stack_trace:
     description: stack_trace
     source: hosted
-    version: "1.4.2"
+    version: "1.5.0"
   string_scanner:
     description: string_scanner
     source: hosted
@@ -228,11 +228,11 @@
   syncbase:
     description: syncbase
     source: hosted
-    version: "0.0.9"
+    version: "0.0.11"
   test:
     description: test
     source: hosted
-    version: "0.12.4+9"
+    version: "0.12.5+1"
   typed_data:
     description: typed_data
     source: hosted
diff --git a/dart/pubspec.yaml b/dart/pubspec.yaml
index a8d7ee0..0d49473 100644
--- a/dart/pubspec.yaml
+++ b/dart/pubspec.yaml
@@ -1,7 +1,7 @@
 name: syncslides
 description: A simple multi-device presentation system built on Flutter and Syncbase.
 dependencies:
-  flutter: ">=0.0.2 <0.1.0"
+  flutter: ">=0.0.16 <0.1.0"
   syncbase: ">=0.0.9 <0.1.0"
 dev_dependencies:
   sky_tools: ">=0.0.27 <0.1.0"