SyncSlides: Initial app setup with some basic
functionality implemented.
With this CL, one can run SyncSlides in either
memory-based store or Syncbase store and view a
list of sample Decks and list of slides for each
deck.
The decks are demo data and loaded by demo-specific
loader. We can have SdCardLoader, GoogleDriveLoader, etc
later on. The demo loader also randomly adds/removes
decks periodically.
Screenshots: https://goo.gl/photos/4R6Gg5b5cMBGDGgP9
Change-Id: I9e6d5ca1442e42eb90ce4a49f0357b56a923f935
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"