syncslides: Basic SlideShow view.
-memory and syncbase APIs for current slide number.
-basic slideshow view with next/prev buttons.
-start of setup for syncing slidenum by allowing
two devices to run at the same time.
Change-Id: I1c5761fbddb726ebb7db13e76f485cd1f7c4894c
diff --git a/dart/Makefile b/dart/Makefile
index d8381d3..88eef82 100644
--- a/dart/Makefile
+++ b/dart/Makefile
@@ -1,3 +1,16 @@
+ifdef DEVICE_NUM
+
+ifneq ($(DEVICE_NUM), 1)
+ REUSE_FLAG := --reuse-server
+endif
+
+DEVICE_NUM_PLUS_ONE := $(shell echo $(DEVICE_NUM) \+ 1 | bc)
+DEVICE_ID := $(shell adb devices | sed -n $(DEVICE_NUM_PLUS_ONE)p | awk '{ print $$1; }')
+DEVICE_FLAG := --target-device $(DEVICE_ID)
+
+endif
+
+
default: run
@@ -16,8 +29,21 @@
upgrade-packages:
pub upgrade
+# Usage example:
+# DEVICE_NUM=1 make run
+# DEVICE_NUM=2 make run
run: packages
- 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
+ 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 --v23.namespace.root=/ns.dev.v.io:8101 $(NAME_FLAG) --logtostderr=true --root-dir=/data/data/org.chromium.mojo.shell/app_home/syncbasedata" $(DEVICE_FLAG) --no-config-file $(REUSE_FLAG)
+
+# Helper targets
+run1:
+ DEVICE_NUM=1 make run
+run2:
+ DEVICE_NUM=2 make run
+run3:
+ DEVICE_NUM=3 make run
+run4:
+ DEVICE_NUM=4 make run
.PHONY: clean
clean:
diff --git a/dart/lib/components/deckgrid.dart b/dart/lib/components/deckgrid.dart
index 5ed43ab..410dad5 100644
--- a/dart/lib/components/deckgrid.dart
+++ b/dart/lib/components/deckgrid.dart
@@ -63,20 +63,15 @@
// 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>();
+Expando<Widget> _weakDeckItemCache = new Expando<Widget>();
Widget _buildDeckBox(BuildContext context, model.Deck deckData) {
- var cachedWidget = weakDeckItemCache[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 thumbnail =
+ new RawImage(bytes: new Uint8List.fromList(deckData.thumbnail));
var title = new Text(deckData.name, style: style.Text.titleStyle);
var titleAndActions =
@@ -91,6 +86,6 @@
builder: (context) => new SlideListPage(deckData.key, deckData.name)));
});
- weakSlideCache[deckData] = gridItem;
+ _weakDeckItemCache[deckData] = gridItem;
return gridItem;
}
diff --git a/dart/lib/components/slidelist.dart b/dart/lib/components/slidelist.dart
index 6cfbd68..49cb4c5 100644
--- a/dart/lib/components/slidelist.dart
+++ b/dart/lib/components/slidelist.dart
@@ -11,20 +11,22 @@
import '../utils/keyvalue.dart';
+import 'slideshow.dart';
+
// SlideListPage is the full page view of the list of slides for a deck.
class SlideListPage extends StatelessComponent {
- String _deckId;
- String _title;
+ final String deckId;
+ final String title;
- SlideListPage(this._deckId, this._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)));
+ center: new Text(title)),
+ body: new Material(child: new SlideList(deckId)));
}
}
@@ -37,7 +39,6 @@
}
class _SlideListState extends State<SlideList> {
- _SlideListState();
Store _store = new Store.singleton();
List<model.Slide> _slides = new List<model.Slide>();
@@ -47,21 +48,28 @@
});
}
+ @override
void initState() {
super.initState();
_store.getAllSlides(config.deckId).then(updateSlides);
+ // TODO(aghassemi): Gracefully handle when deck is deleted while in this view.
}
Widget build(BuildContext context) {
// Create a list of <SlideNumber, Slide> pairs.
- List<KeyValue<String, model.Slide>> slidesWithPosition = [];
+ List<KeyValue<int, model.Slide>> slidesWithPosition = [];
for (var i = 0; i < _slides.length; i++) {
- slidesWithPosition.add(new KeyValue(i.toString(), _slides[i]));
+ slidesWithPosition.add(new KeyValue(i, _slides[i]));
}
return new ScrollableList(
itemExtent: style.Size.listHeight,
items: slidesWithPosition,
- itemBuilder: (context, kv) => _buildSlide(context, kv.key, kv.value));
+ itemBuilder: (context, kv) =>
+ _buildSlide(context, kv.key.toString(), kv.value, onTap: () {
+ _store.setCurrSlideNum(config.deckId, kv.key);
+ Navigator.of(context).push(new PageRoute(
+ builder: (context) => new SlideshowPage(config.deckId)));
+ }));
}
}
@@ -69,23 +77,19 @@
// 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];
+Expando<Widget> _weakSlideCache = new Expando<Widget>();
+Widget _buildSlide(BuildContext context, String key, model.Slide slideData,
+ {Function onTap}) {
+ 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');
- }
+ var thumbnail = new RawImage(
+ height: style.Size.listHeight,
+ bytes: new Uint8List.fromList(slideData.image),
+ fit: ImageFit.cover);
+
thumbnail = new Flexible(child: thumbnail);
var title = new Text('Slide $key', style: style.Text.subTitleStyle);
@@ -100,8 +104,8 @@
child: new Card(child: new Row([thumbnail, titleAndNotes])),
margin: style.Spacing.listItemMargin);
- var listItem = new InkWell(key: new Key(key), child: card);
+ var listItem = new InkWell(key: new Key(key), child: card, onTap: onTap);
- weakSlideCache[slideData] = listItem;
+ _weakSlideCache[slideData] = listItem;
return listItem;
}
diff --git a/dart/lib/components/slideshow.dart b/dart/lib/components/slideshow.dart
new file mode 100644
index 0000000..1c48abc
--- /dev/null
+++ b/dart/lib/components/slideshow.dart
@@ -0,0 +1,92 @@
+// 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/widgets.dart';
+import 'package:flutter/material.dart';
+
+import '../models/all.dart' as model;
+import '../stores/store.dart';
+
+class SlideshowPage extends StatelessComponent {
+ final String deckId;
+
+ SlideshowPage(this.deckId);
+
+ Widget build(BuildContext context) {
+ return new Scaffold(
+ toolBar: new ToolBar(
+ left: new IconButton(
+ icon: 'navigation/arrow_back',
+ onPressed: () => Navigator.of(context).pop())),
+ body: new Material(child: new SlideShow(deckId)));
+ }
+}
+
+class SlideShow extends StatefulComponent {
+ final String deckId;
+ SlideShow(this.deckId);
+
+ _SlideShowState createState() => new _SlideShowState();
+}
+
+class _SlideShowState extends State<SlideShow> {
+ Store _store = new Store.singleton();
+ List<model.Slide> _slides;
+ int _currSlideNum = 0;
+ StreamSubscription _onChangeSubscription;
+
+ void updateSlides(List<model.Slide> slides) {
+ setState(() {
+ _slides = slides;
+ });
+ }
+
+ void updateCurrSlideNum(int newCurr) {
+ setState(() {
+ _currSlideNum = newCurr;
+ });
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ _store.getAllSlides(config.deckId).then(updateSlides);
+ _store.getCurrSlideNum(config.deckId).then(updateCurrSlideNum);
+ _onChangeSubscription =
+ _store.onCurrSlideNumChange(config.deckId).listen(updateCurrSlideNum);
+ // TODO(aghassemi): Gracefully handle when deck is deleted during Slideshow
+ }
+
+ @override
+ void dispose() {
+ // Stop listening to updates from store when component is disposed.
+ _onChangeSubscription.cancel();
+ super.dispose();
+ }
+
+ Widget build(BuildContext context) {
+ if (_slides == null) {
+ // TODO(aghassemi): Remove when store operations become sync.
+ return new Text('Loading');
+ }
+ var slideData = _slides[_currSlideNum];
+ var image = new RawImage(
+ bytes: new Uint8List.fromList(slideData.image), fit: ImageFit.contain);
+
+ return new Block([
+ image,
+ new Text(_currSlideNum.toString()),
+ new Row([
+ new FlatButton(child: new Text("Prev"), onPressed: () {
+ _store.setCurrSlideNum(config.deckId, _currSlideNum - 1);
+ }),
+ new FlatButton(child: new Text("Next"), onPressed: () {
+ _store.setCurrSlideNum(config.deckId, _currSlideNum + 1);
+ })
+ ])
+ ]);
+ }
+}
diff --git a/dart/lib/loaders/demo_loader.dart b/dart/lib/loaders/demo_loader.dart
index ad0a089..94d0243 100644
--- a/dart/lib/loaders/demo_loader.dart
+++ b/dart/lib/loaders/demo_loader.dart
@@ -27,10 +27,10 @@
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('pitch$i', 'Pitch Deck #$i',
+ await _getRawBytes('assets/images/sample_decks/pitch/thumb.png'));
yield new model.Deck('vanadium$i', 'Vanadium #$i',
await _getRawBytes('assets/images/sample_decks/vanadium/thumb.png'));
}
@@ -58,7 +58,11 @@
var removeDeck = _rand.nextBool();
if (removeDeck && decks.length > 0) {
- _store.removeDeck(decks[_rand.nextInt(decks.length)].key);
+ var rIndex = _rand.nextInt(decks.length);
+ // Never delete the first deck so we can safely use it for slideshow
+ if (rIndex >= 1) {
+ _store.removeDeck(decks[rIndex].key);
+ }
} else {
await for (var deck in _getSampleDecks()) {
if (!deckKeys.contains(deck.key)) {
diff --git a/dart/lib/models/deck.dart b/dart/lib/models/deck.dart
index 6e389f1..133dc04 100644
--- a/dart/lib/models/deck.dart
+++ b/dart/lib/models/deck.dart
@@ -31,4 +31,6 @@
map['thumbnail'] = thumbnail;
return JSON.encode(map);
}
+
+ // TODO(aghassemi): Override == and hash
}
diff --git a/dart/lib/models/slide.dart b/dart/lib/models/slide.dart
index ec24128..ebb665a 100644
--- a/dart/lib/models/slide.dart
+++ b/dart/lib/models/slide.dart
@@ -21,4 +21,6 @@
map['image'] = image;
return JSON.encode(map);
}
+
+ // TODO(aghassemi): Override == and hash
}
diff --git a/dart/lib/stores/keyutil.dart b/dart/lib/stores/keyutil.dart
index d2cd08b..eb543c8 100644
--- a/dart/lib/stores/keyutil.dart
+++ b/dart/lib/stores/keyutil.dart
@@ -7,12 +7,36 @@
return '$deckId/slides/$slideIndex';
}
-// Constructs prefix key for a deck.
-String getDeckKeyPrefix(String deckKey) {
- return deckKey + '/';
+// Constructs a key prefix for all slides of a deck.
+String getSlidesKeyPrefix(String deckId) {
+ return getDeckKeyPrefix(deckId) + 'slides/';
+}
+
+// Constructs a key prefix for a deck.
+String getDeckKeyPrefix(String deckId) {
+ return deckId + '/';
}
// Returns true if a key is for a deck.
bool isDeckKey(String key) {
return !key.contains('/');
}
+
+// Constructs a current slide number key.
+String getCurrSlideNumKey(String deckId) {
+ return '$deckId/currslidenum';
+}
+
+// Gets the deck id given a current slide number key.
+String currSlideNumKeyToDeckId(String currSlideNumKey) {
+ if ((!isCurrSlideNumKey(currSlideNumKey))) {
+ throw new ArgumentError(
+ "$currSlideNumKey is not a valid current slide number key.");
+ }
+ return currSlideNumKey.substring(0, currSlideNumKey.indexOf('/currslidenum'));
+}
+
+// Returns true if a key is a current slide number key.
+bool isCurrSlideNumKey(String key) {
+ return key.endsWith('/currslidenum');
+}
diff --git a/dart/lib/stores/memory_store.dart b/dart/lib/stores/memory_store.dart
index 5c092c0..aa80f74 100644
--- a/dart/lib/stores/memory_store.dart
+++ b/dart/lib/stores/memory_store.dart
@@ -11,14 +11,21 @@
// A memory-based implementation of Store.
class MemoryStore implements Store {
- StreamController _onDecksChangeController;
+ StreamController _onDecksChangeEmitter;
Map<String, String> _decksMap;
Map<String, String> _slidesMap;
+ Map<String, int> _currSlideNumMap;
+ Map<String, StreamController> _currSlideNumChangeEmitterMap;
MemoryStore()
- : _onDecksChangeController = new StreamController.broadcast(),
+ : _onDecksChangeEmitter = new StreamController.broadcast(),
_decksMap = new Map(),
- _slidesMap = new Map();
+ _slidesMap = new Map(),
+ _currSlideNumMap = new Map(),
+ _currSlideNumChangeEmitterMap = new Map();
+
+ //////////////////////////////////////
+ /// Decks
Future<List<model.Deck>> getAllDecks() async {
var decks = [];
@@ -32,7 +39,7 @@
Future addDeck(model.Deck deck) async {
var json = deck.toJson();
_decksMap[deck.key] = json;
- getAllDecks().then(_triggerDecksChangeEvent);
+ getAllDecks().then(_onDecksChangeEmitter.add);
}
Future removeDeck(String deckKey) async {
@@ -42,10 +49,13 @@
slideKey.startsWith(keyutil.getDeckKeyPrefix(deckKey)))
.toList()
.forEach(_slidesMap.remove);
- getAllDecks().then(_triggerDecksChangeEvent);
+ getAllDecks().then(_onDecksChangeEmitter.add);
}
- Stream<List<model.Deck>> get onDecksChange => _onDecksChangeController.stream;
+ Stream<List<model.Deck>> get onDecksChange => _onDecksChangeEmitter.stream;
+
+ //////////////////////////////////////
+ /// Slides
Future<List<model.Slide>> getAllSlides(String deckKey) async {
var slides = [];
@@ -65,7 +75,28 @@
}
}
- _triggerDecksChangeEvent(List<model.Deck> decks) {
- _onDecksChangeController.add(decks);
+ //////////////////////////////////////
+ // Slideshow
+
+ Future<int> getCurrSlideNum(String deckId) async {
+ return _currSlideNumMap[deckId] ?? 0;
+ }
+
+ Future setCurrSlideNum(String deckId, int slideNum) async {
+ var slides = await getAllSlides(deckId);
+ if (slideNum >= 0 && slideNum < slides.length) {
+ _currSlideNumMap[deckId] = slideNum;
+ _getCurrSlideNumChangeEmitter(deckId).add(slideNum);
+ }
+ }
+
+ Stream<int> onCurrSlideNumChange(String deckId) {
+ return _getCurrSlideNumChangeEmitter(deckId).stream;
+ }
+
+ StreamController _getCurrSlideNumChangeEmitter(String deckId) {
+ _currSlideNumChangeEmitterMap.putIfAbsent(
+ deckId, () => new StreamController.broadcast());
+ return _currSlideNumChangeEmitterMap[deckId];
}
}
diff --git a/dart/lib/stores/store.dart b/dart/lib/stores/store.dart
index 4dec8b5..06eadfd 100644
--- a/dart/lib/stores/store.dart
+++ b/dart/lib/stores/store.dart
@@ -8,6 +8,13 @@
import 'store_factory.dart' as storeFactory;
+// TODO(aghassemi): Make all store operation synchronous.
+// Current pattern of components needing to call async methods and keep and
+// update their own state is already becoming messy. When store becomes
+// synchronous, then these components can simply use _store.getSlides(),
+// _store.getCurrSlide(), etc.. directly in their renderer and do not need to
+// keep any state of their own.
+
// Provides APIs for reading and writing app-related data.
abstract class Store {
static Store _singletonStore;
@@ -43,4 +50,13 @@
// Sets the slides for a deck.
Future setSlides(String deckKey, List<model.Slide> slides);
+
+ //////////////////////////////////////
+ // Slideshow
+
+ Future<int> getCurrSlideNum(String deckId);
+
+ Future setCurrSlideNum(String deckId, int slideNum);
+
+ Stream<int> onCurrSlideNumChange(String deckId);
}
diff --git a/dart/lib/stores/syncbase_store.dart b/dart/lib/stores/syncbase_store.dart
index 5777bec..2bfd564 100644
--- a/dart/lib/stores/syncbase_store.dart
+++ b/dart/lib/stores/syncbase_store.dart
@@ -7,6 +7,7 @@
import '../models/all.dart' as model;
import '../syncbase/client.dart' as sb;
+import '../utils/errors.dart' as errorsutil;
import 'keyutil.dart' as keyutil;
import 'store.dart';
@@ -15,14 +16,21 @@
// Implementation of using Syncbase (http://v.io/syncbase) storage system.
class SyncbaseStore implements Store {
- StreamController _onDecksChangeController;
+ StreamController _deckChangeEmitter;
+ Map<String, StreamController> _currSlideNumChangeEmitterMap;
+
SyncbaseStore() {
- _onDecksChangeController = new StreamController.broadcast();
- _onDecksChangeController.onListen = () {
- sb.getDatabase().then(_startDecksWatch);
- };
+ _deckChangeEmitter = new StreamController.broadcast();
+ _currSlideNumChangeEmitterMap = new Map();
+ sb.getDatabase().then((db) {
+ _startDecksWatch(db);
+ _startSync(db);
+ });
}
+ //////////////////////////////////////
+ /// Decks
+
Future<List<model.Deck>> getAllDecks() async {
// Key schema is:
// <deckId> --> Deck
@@ -48,7 +56,19 @@
tb.deleteRange(new sb.RowRange.prefix(deckKey));
}
- Stream<List<model.Deck>> get onDecksChange => _onDecksChangeController.stream;
+ Stream<List<model.Deck>> get onDecksChange => _deckChangeEmitter.stream;
+
+ 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);
+ }
+
+ //////////////////////////////////////
+ /// Slides
Future<List<model.Slide>> getAllSlides(String deckKey) async {
// Key schema is:
@@ -57,7 +77,7 @@
// 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 prefix = keyutil.getSlidesKeyPrefix(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();
@@ -74,13 +94,53 @@
}
}
+ model.Slide _toSlide(List<List<int>> row) {
+ var value = UTF8.decode(row[1]);
+ return new model.Slide.fromJson(value);
+ }
+
+ //////////////////////////////////////
+ // Slideshow
+
+ Future<int> getCurrSlideNum(String deckId) async {
+ sb.SyncbaseTable tb = await _getDecksTable();
+ var v = await tb.get(keyutil.getCurrSlideNumKey(deckId));
+ if (v == null || v.isEmpty) {
+ return 0;
+ }
+ return v[0];
+ }
+
+ Future setCurrSlideNum(String deckId, int slideNum) async {
+ sb.SyncbaseTable tb = await _getDecksTable();
+ var slides = await getAllSlides(deckId);
+ if (slideNum >= 0 && slideNum < slides.length) {
+ // TODO(aghassemi): Move outside of decks table and into a schema just for
+ // storing UI state.
+ await tb.put(keyutil.getCurrSlideNumKey(deckId), [slideNum]);
+ }
+ }
+
+ Stream<int> onCurrSlideNumChange(String deckId) {
+ return _getCurrSlideNumChangeEmitter(deckId).stream;
+ }
+
+ StreamController _getCurrSlideNumChangeEmitter(String deckId) {
+ _currSlideNumChangeEmitterMap.putIfAbsent(
+ deckId, () => new StreamController.broadcast());
+ return _currSlideNumChangeEmitterMap[deckId];
+ }
+
Future<sb.SyncbaseTable> _getDecksTable() async {
sb.SyncbaseNoSqlDatabase sbDb = await sb.getDatabase();
sb.SyncbaseTable tb = sbDb.table(decksTableName);
- if (await tb.exists()) {
- return tb;
+ try {
+ await tb.create(sb.createOpenPerms());
+ } catch (e) {
+ if (!errorsutil.isExistsError(e)) {
+ throw e;
+ }
}
- await tb.create(sb.createOpenPerms());
return tb;
}
@@ -88,36 +148,28 @@
var resumeMarker = await sbDb.getResumeMarker();
var stream = sbDb.watch(decksTableName, '', resumeMarker);
- var streamListener = stream.listen((sb.WatchChange change) async {
+ 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);
+ if (!_deckChangeEmitter.isPaused || !_deckChangeEmitter.isClosed) {
+ var decks = await getAllDecks();
+ _deckChangeEmitter.add(decks);
+ }
+ } else if (keyutil.isCurrSlideNumKey(change.rowKey)) {
+ var deckId = keyutil.currSlideNumKeyToDeckId(change.rowKey);
+ var emitter = _getCurrSlideNumChangeEmitter(deckId);
+ if (!emitter.isPaused || !emitter.isClosed) {
+ if (change.changeType == sb.WatchChangeTypes.put) {
+ int currSlideNum = change.valueBytes[0];
+ emitter.add(currSlideNum);
+ } else {
+ emitter.add(0);
+ }
+ }
}
});
-
- // 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);
- }
+ Future _startSync(sb.SyncbaseNoSqlDatabase sbDb) async {}
}
diff --git a/dart/lib/syncbase/client.dart b/dart/lib/syncbase/client.dart
index b64ad7f..4a25a22 100644
--- a/dart/lib/syncbase/client.dart
+++ b/dart/lib/syncbase/client.dart
@@ -7,6 +7,8 @@
import 'package:flutter/services.dart' show shell;
import 'package:syncbase/syncbase_client.dart';
+import '../utils/errors.dart' as errorsutil;
+
export 'package:syncbase/syncbase_client.dart';
const String syncbaseMojoUrl =
@@ -33,19 +35,27 @@
Future<SyncbaseApp> _createApp(SyncbaseClient sbClient) async {
var app = sbClient.app(appName);
- if (await app.exists()) {
- return app;
+ try {
+ await app.create(createOpenPerms());
+ } catch (e) {
+ if (!errorsutil.isExistsError(e)) {
+ throw e;
+ }
}
- await app.create(createOpenPerms());
+
return app;
}
Future<SyncbaseNoSqlDatabase> _createDb(SyncbaseApp app) async {
var db = app.noSqlDatabase(dbName);
- if (await db.exists()) {
- return db;
+ try {
+ await db.create(createOpenPerms());
+ } catch (e) {
+ if (!errorsutil.isExistsError(e)) {
+ throw e;
+ }
}
- await db.create(createOpenPerms());
+
return db;
}
diff --git a/dart/lib/utils/errors.dart b/dart/lib/utils/errors.dart
new file mode 100644
index 0000000..a9add77
--- /dev/null
+++ b/dart/lib/utils/errors.dart
@@ -0,0 +1,10 @@
+// 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.
+
+const String existsErrorId = 'v.io/v23/verror.Exist';
+
+// TODO(aghassemi): Export mojo.Error in Syncbase and use the type here.
+bool isExistsError(e) {
+ return e != null && (e.id == existsErrorId);
+}