Syncslides: MVP1 feature set and data flow refactoring
This CL includes the feature set for MVP1, polish, specially
around the Slideshow view, bug and performance fixes remain.
-Decks now sync between devices.
-Presentation can be drived by the presenter.
-Audience can follow a presentation or navigate
on their own and sync back when they want.
-Toast messages for starting and joining a presentation.
-Proper back button navigation stack.
-Big overhaul of the UI rendering and data flow:
+Introduction of an in-memory, publically deeply-immutable
state object that rendering is purely based on.
+Using watch to update the in-memory state after fetching
the initial values from Syncbase.
+Clear definitions for all the possible app actions and state.
+This change makes the unidirectional data flow simpler to
understand and maintain and removed many potential concurrency
issues of the previous design.
Change-Id: I7ed07a75f56693931efa5fcdfc43ce4e6e544fa9
diff --git a/.gitignore b/.gitignore
index a752fc6..faa2bc5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,5 @@
/dart/.packages
/dart/snapshot_blob.bin
/dart/app.flx
+/dart/build
+.atom
diff --git a/dart/FLUTTER_VERSION b/dart/FLUTTER_VERSION
index 8383605..7c3d0f3 100644
--- a/dart/FLUTTER_VERSION
+++ b/dart/FLUTTER_VERSION
@@ -1 +1 @@
-04dfa0bf87a37eaeef0d85aeff0c7ea7ca04782d
+ee8c0ad30d60ac965c243f7bf982527736d54cd8
diff --git a/dart/MOJO_VERSION b/dart/MOJO_VERSION
index df87133..a8fb06d 100644
--- a/dart/MOJO_VERSION
+++ b/dart/MOJO_VERSION
@@ -1 +1 @@
-7a690c5c0344946ed74402d61d473502bc03ad77
+08b1d8fc1e0296569b628cae2e7611988497b273
\ No newline at end of file
diff --git a/dart/Makefile b/dart/Makefile
index be5398d..5ae2d85 100644
--- a/dart/Makefile
+++ b/dart/Makefile
@@ -4,6 +4,10 @@
ifneq ($(DEVICE_NUM), 1)
REUSE_FLAG := --reuse-servers
+else
+ # TODO(aghassemi): This needs to become $(DEVICE_ID) when we have a way to
+ # send the $(DEVICE_ID) to the Flutter app.
+ NAME_FLAG := --name=syncslides
endif
ifdef VLOG
@@ -15,6 +19,7 @@
DEVICE_ID := $(shell adb devices | sed -n $(DEVICE_NUM_PLUS_ONE)p | awk '{ print $$1; }')
DEVICE_FLAG := --target-device $(DEVICE_ID)
MOUNTTABLE_ADDR := /192.168.86.254:8101
+ANDROID_CREDS_DIR := /sdcard/v23creds
default: run
@@ -45,6 +50,7 @@
--args-for="https://syncslides.mojo.v.io/packages/syncbase/mojo_services/android/syncbase_server.mojo \
--root-dir=$(SYNCBASE_DATA_DIR) \
--v23.namespace.root=$(MOUNTTABLE_ADDR) \
+ $(NAME_FLAG) \
$(VLOG_FLAGS)" \
$(DEVICE_FLAG) \
$(REUSE_FLAG) \
diff --git a/dart/flutter.yaml b/dart/flutter.yaml
index 00911c6..47b433e 100644
--- a/dart/flutter.yaml
+++ b/dart/flutter.yaml
@@ -3,6 +3,7 @@
- name: navigation/arrow_back
- name: navigation/arrow_forward
- name: content/add
+ - name: notification/sync
assets:
- assets/images/defaults/thumbnail.png
- assets/images/sample_decks/vanadium/1.jpg
diff --git a/dart/lib/components/deckgrid.dart b/dart/lib/components/deckgrid.dart
index 441db75..71771e3 100644
--- a/dart/lib/components/deckgrid.dart
+++ b/dart/lib/components/deckgrid.dart
@@ -2,137 +2,138 @@
// 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 '../loaders/loader.dart';
import '../models/all.dart' as model;
import '../stores/store.dart';
import '../styles/common.dart' as style;
import '../utils/image_provider.dart' as imageProvider;
-
import 'slidelist.dart';
+import 'slideshow.dart';
+import 'syncslides_page.dart';
+import 'toast.dart' as toast;
+
+final GlobalKey _scaffoldKey = new GlobalKey();
// DeckGridPage is the full page view of the list of decks.
-class DeckGridPage extends StatelessComponent {
- Loader _loader = new Loader.singleton();
- Widget build(BuildContext context) {
+class DeckGridPage extends SyncSlidesPage {
+ initState(AppState appState, AppActions appActions) {
+ appActions.stopAllPresentations();
+ }
+
+ Widget build(BuildContext context, AppState appState, AppActions appActions) {
+ List<model.Deck> decks = appState.decks.values
+ .where((DeckState d) => d.deck != null)
+ .map((d) => d.deck);
+ List<model.PresentationAdvertisement> presentations =
+ appState.presentationAdvertisements.values;
+
return new Scaffold(
+ key: _scaffoldKey,
toolBar: new ToolBar(center: new Text('SyncSlides')),
floatingActionButton: new FloatingActionButton(
child: new Icon(icon: 'content/add'), onPressed: () {
- _loader.addDeck();
+ appActions.loadDemoDeck();
}),
- body: new Material(child: new DeckGrid()));
+ body: new Material(
+ child: new DeckGrid(decks, presentations, appActions)));
}
}
// DeckGrid is scrollable grid view of decks.
-class DeckGrid extends StatefulComponent {
- _DeckGridState createState() => new _DeckGridState();
-}
+class DeckGrid extends StatelessComponent {
+ AppActions _appActions;
+ List<model.Deck> _decks;
+ List<model.PresentationAdvertisement> _presentations;
-class _DeckGridState extends State<DeckGrid> {
- Store _store = new Store.singleton();
- List<model.Deck> _decks = new List<model.Deck>();
- StreamSubscription _onDecksChangeSubscription;
- StreamSubscription _onStateChangeSubscription;
-
- void updateDecks(List<model.Deck> decks) {
- setState(() {
- _decks = decks;
- });
- }
-
- void _rebuild(_) {
- setState(() {});
- }
-
- @override
- void initState() {
- // Stop all active presentations when coming back to the decks grid page.
- _store.stopAllPresentations();
- _store.getAllDecks().then(updateDecks);
- // Update the state whenever store tells us decks have changed.
- _onDecksChangeSubscription = _store.onDecksChange.listen(updateDecks);
- _onStateChangeSubscription = _store.onStateChange.listen(_rebuild);
- super.initState();
- }
-
- @override
- void dispose() {
- // Stop listening to updates from store when component is disposed.
- _onDecksChangeSubscription.cancel();
- _onStateChangeSubscription.cancel();
- super.dispose();
- }
+ DeckGrid(this._decks, this._presentations, this._appActions);
Widget build(BuildContext context) {
List<Widget> deckBoxes = _decks.map((deck) => _buildDeckBox(context, deck));
- List<Widget> presentationBoxes = _store.state.livePresentations
+ List<Widget> presentationBoxes = _presentations
.map((presentation) => _buildPresentationBox(context, presentation));
var allBoxes = new List.from(presentationBoxes)..addAll(deckBoxes);
var grid = new Grid(allBoxes, maxChildExtent: style.Size.thumbnailWidth);
return new ScrollableViewport(child: grid);
}
-}
-Widget _buildDeckBox(BuildContext context, model.Deck deckData) {
- var thumbnail =
- new AsyncImage(provider: imageProvider.getDeckThumbnailImage(deckData));
- // TODO(aghassemi): Add "Opened on" data.
- var subtitleWidget =
- new Text("Opened on Sep 12, 2015", style: style.Text.subtitleStyle);
- subtitleWidget = _stopWrapping(subtitleWidget);
- var footer = _buildBoxFooter(deckData.name, subtitleWidget);
- var box = _buildCard(deckData.key, [thumbnail, footer], () {
- Navigator.of(context).push(new MaterialPageRoute(
- builder: (context) => new SlideListPage(deckData.key, deckData.name)));
- });
+ Widget _buildDeckBox(BuildContext context, model.Deck deckData) {
+ var thumbnail =
+ new AsyncImage(provider: imageProvider.getDeckThumbnailImage(deckData));
+ // TODO(aghassemi): Add "Opened on" data.
+ var subtitleWidget =
+ new Text("Opened on Sep 12, 2015", style: style.Text.subtitleStyle);
+ subtitleWidget = _stopWrapping(subtitleWidget);
+ var footer = _buildBoxFooter(deckData.name, subtitleWidget);
+ var box = _buildCard(deckData.key, [thumbnail, footer], () {
+ Navigator.of(context).push(new MaterialPageRoute(
+ builder: (context) => new SlideListPage(deckData.key)));
+ });
- return box;
-}
+ return box;
+ }
-Widget _buildPresentationBox(
- BuildContext context, model.PresentationAdvertisement presentationData) {
- var thumbnail = new AsyncImage(
- provider: imageProvider.getDeckThumbnailImage(presentationData.deck));
- var liveBox = new Row([
- new Container(
- child: new Text("LIVE NOW", style: style.Text.liveNow),
- decoration: style.Box.liveNow,
- margin: style.Spacing.normalMargin,
- padding: style.Spacing.extraSmallPadding)
- ]);
- var footer = _buildBoxFooter(presentationData.deck.name, liveBox);
- var box = _buildCard(presentationData.key, [thumbnail, footer], () {});
- return box;
-}
+ Widget _buildPresentationBox(
+ BuildContext context, model.PresentationAdvertisement presentationData) {
+ var thumbnail = new AsyncImage(
+ provider: imageProvider.getDeckThumbnailImage(presentationData.deck));
+ var liveBox = new Row([
+ new Container(
+ child: new Text("LIVE NOW", style: style.Text.liveNow),
+ decoration: style.Box.liveNow,
+ margin: style.Spacing.normalMargin,
+ padding: style.Spacing.extraSmallPadding)
+ ]);
+ var footer = _buildBoxFooter(presentationData.deck.name, liveBox);
+ var box = _buildCard(presentationData.key, [thumbnail, footer], () async {
+ toast.info(
+ _scaffoldKey, 'Joining presentation ${presentationData.deck.name}...',
+ duration: toast.Durations.permanent);
+ try {
+ await _appActions.joinPresentation(presentationData);
-Widget _buildBoxFooter(String title, Widget subtitle) {
- var titleWidget = new Text(title, style: style.Text.titleStyle);
- titleWidget = _stopWrapping(titleWidget);
+ toast.info(
+ _scaffoldKey, 'Joined presentation ${presentationData.deck.name}.');
- var titleAndSubtitle = new Block([titleWidget, subtitle]);
- return new Container(
- child: titleAndSubtitle, padding: style.Spacing.normalPadding);
-}
+ // Push slides list page first before navigating to the slideshow.
+ Navigator.of(context).push(new MaterialPageRoute(
+ builder: (context) => new SlideListPage(presentationData.deck.key,
+ presentationId: presentationData.key)));
+ Navigator.of(context).push(new MaterialPageRoute(
+ builder: (context) => new SlideshowPage(presentationData.deck.key,
+ presentationId: presentationData.key)));
+ } catch (e) {
+ toast.error(_scaffoldKey,
+ 'Failed to start presentation ${presentationData.deck.name}.', e);
+ }
+ });
+ return box;
+ }
-Widget _buildCard(String key, List<Widget> children, Function onTap) {
- var content = new Container(
- child: new Card(child: new Block(children)),
- margin: style.Spacing.normalMargin);
+ Widget _buildBoxFooter(String title, Widget subtitle) {
+ var titleWidget = new Text(title, style: style.Text.titleStyle);
+ titleWidget = _stopWrapping(titleWidget);
- return new InkWell(key: new Key(key), child: content, onTap: onTap);
-}
+ var titleAndSubtitle = new Block([titleWidget, subtitle]);
+ return new Container(
+ child: titleAndSubtitle, padding: style.Spacing.normalPadding);
+ }
-Widget _stopWrapping(Text child) {
- // TODO(aghassemi): There is no equivalent of CSS's white-space: nowrap,
- // overflow: hidden or text-overflow: ellipsis in Flutter yet.
- // This workaround simulates white-space: nowrap and overflow: hidden.
- // See https://github.com/flutter/flutter/issues/417
- return new Viewport(
- child: child, scrollDirection: ScrollDirection.horizontal);
+ Widget _buildCard(String key, List<Widget> children, Function onTap) {
+ var content = new Container(
+ child: new Card(child: new Block(children)),
+ margin: style.Spacing.normalMargin);
+
+ return new InkWell(key: new Key(key), child: content, onTap: onTap);
+ }
+
+ Widget _stopWrapping(Text child) {
+ // TODO(aghassemi): There is no equivalent of CSS's white-space: nowrap,
+ // overflow: hidden or text-overflow: ellipsis in Flutter yet.
+ // This workaround simulates white-space: nowrap and overflow: hidden.
+ // See https://github.com/flutter/flutter/issues/417
+ return new Viewport(
+ child: child, scrollDirection: ScrollDirection.horizontal);
+ }
}
diff --git a/dart/lib/components/slidelist.dart b/dart/lib/components/slidelist.dart
index 49ead9b..a680b35 100644
--- a/dart/lib/components/slidelist.dart
+++ b/dart/lib/components/slidelist.dart
@@ -2,97 +2,117 @@
// 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/image_provider.dart' as imageProvider;
-
import 'slideshow.dart';
+import 'syncslides_page.dart';
+import 'toast.dart' as toast;
-// SlideListPage is the full page view of the list of slides for a deck.
-class SlideListPage extends StatelessComponent {
- final String deckId;
- final String title;
- Store _store = new Store.singleton();
+final GlobalKey _scaffoldKey = new GlobalKey();
- SlideListPage(this.deckId, this.title);
- Widget build(BuildContext context) {
+class SlideListPage extends SyncSlidesPage {
+ final String _deckId;
+ final String _presentationId;
+
+ SlideListPage(this._deckId, {String presentationId})
+ : _presentationId = presentationId;
+
+ Widget build(BuildContext context, AppState appState, AppActions appActions) {
+ if (!appState.decks.containsKey(_deckId)) {
+ // TODO(aghassemi): Proper error page with navigation back to main view.
+ return new Text('Deck no longer exists.');
+ }
+
+ var deckState = appState.decks[_deckId];
+ var slides = deckState.slides;
return new Scaffold(
+ key: _scaffoldKey,
toolBar: new ToolBar(
left: new IconButton(
icon: 'navigation/arrow_back',
onPressed: () => Navigator.of(context).pop()),
- center: new Text(title)),
- floatingActionButton: _buildPresentFab(context),
- body: new Material(child: new SlideList(deckId)));
+ center: new Text(deckState.deck.name)),
+ floatingActionButton: _buildPresentFab(context, appState, appActions),
+ body: new Material(child: new SlideList(_deckId, slides, appActions)));
}
- _buildPresentFab(BuildContext context) {
+ _buildPresentFab(
+ BuildContext context, AppState appState, AppActions appActions) {
+ bool inPresentation = _presentationId != null &&
+ appState.presentations.containsKey(_presentationId);
+ if (inPresentation) {
+ // Can't present when viewing a presentation.
+ return null;
+ }
+
+ bool alreadyAdvertised = appState.advertisedPresentations
+ .any((model.PresentationAdvertisement p) => p.deck.key == _deckId);
+ if (alreadyAdvertised) {
+ return null;
+ }
+
return new FloatingActionButton(
child: new Icon(icon: 'navigation/arrow_forward'), onPressed: () async {
- model.PresentationAdvertisement presentation =
- await _store.startPresentation(deckId);
- Navigator.of(context).push(new MaterialPageRoute(
- builder: (context) => new SlideshowPage(deckId)));
+ toast.info(_scaffoldKey, 'Starting presentation...',
+ duration: toast.Durations.permanent);
+
+ try {
+ var presentation = await appActions.startPresentation(_deckId);
+ toast.info(_scaffoldKey, 'Presentation started.');
+
+ Navigator.of(context).push(new MaterialPageRoute(
+ builder: (context) =>
+ new SlideshowPage(_deckId, presentationId: presentation.key)));
+ } catch (e) {
+ toast.error(_scaffoldKey, 'Failed to start presentation.', e);
+ }
});
}
}
-// 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> {
- Store _store = new Store.singleton();
+class SlideList extends StatelessComponent {
+ String _deckId;
+ String _presentationId;
List<model.Slide> _slides = new List<model.Slide>();
-
- void updateSlides(List<model.Slide> slides) {
- setState(() {
- _slides = slides;
- });
- }
-
- @override
- void initState() {
- super.initState();
- _store.getAllSlides(config.deckId).then(updateSlides);
- // TODO(aghassemi): Gracefully handle when deck is deleted while in this view.
- }
+ AppActions _appActions;
+ SlideList(this._deckId, this._slides, this._appActions,
+ {String presentationId})
+ : _presentationId = presentationId;
Widget build(BuildContext context) {
+ NavigatorState navigator = Navigator.of(context);
return new ScrollableList(
itemExtent: style.Size.listHeight,
items: _slides,
itemBuilder: (context, value, index) =>
- _buildSlide(context, config.deckId, index, index.toString(), value,
- onTap: () {
- _store.setCurrSlideNum(config.deckId, index);
- Navigator.of(context).push(new MaterialPageRoute(
- builder: (context) => new SlideshowPage(config.deckId)));
+ _buildSlide(context, _deckId, index, value, onTap: () {
+ _appActions.setCurrSlideNum(_deckId, index,
+ presentationId: _presentationId);
+
+ navigator.push(new MaterialPageRoute(
+ builder: (context) => new SlideshowPage(_deckId,
+ presentationId: _presentationId)));
}));
}
}
-Widget _buildSlide(BuildContext context, String deckId, int slideIndex,
- String key, model.Slide slideData,
+Widget _buildSlide(
+ BuildContext context, String deckId, int slideIndex, model.Slide slideData,
{Function onTap}) {
var thumbnail = new AsyncImage(
- provider: imageProvider.getSlideImage(deckId, slideIndex, slideData),
+ provider: imageProvider.getSlideImage(deckId, slideData),
height: style.Size.listHeight,
fit: ImageFit.cover);
thumbnail = new Flexible(child: thumbnail);
- var title = new Text('Slide $key', style: style.Text.subtitleStyle);
+ var title = new Text('Slide $slideIndex', style: style.Text.subtitleStyle);
var notes = new Text(
- 'This is the teaser slide. It should be memorable and descriptive');
+ '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),
@@ -102,7 +122,8 @@
child: new Card(child: new Row([thumbnail, titleAndNotes])),
margin: style.Spacing.listItemMargin);
- var listItem = new InkWell(key: new Key(key), child: card, onTap: onTap);
+ var listItem = new InkWell(
+ key: new Key(slideIndex.toString()), child: card, onTap: onTap);
return listItem;
}
diff --git a/dart/lib/components/slideshow.dart b/dart/lib/components/slideshow.dart
index 700ef5b..cf301f9 100644
--- a/dart/lib/components/slideshow.dart
+++ b/dart/lib/components/slideshow.dart
@@ -2,100 +2,129 @@
// 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 'package:flutter/widgets.dart';
+import 'package:logging/logging.dart';
import '../models/all.dart' as model;
import '../stores/store.dart';
import '../styles/common.dart' as style;
import '../utils/image_provider.dart' as imageProvider;
+import 'syncslides_page.dart';
-class SlideshowPage extends StatelessComponent {
- final String deckId;
+final Logger log = new Logger('store/syncbase_store');
- SlideshowPage(this.deckId);
+class SlideshowPage extends SyncSlidesPage {
+ final String _deckId;
+ final String _presentationId;
- Widget build(BuildContext context) {
+ SlideshowPage(this._deckId, {String presentationId})
+ : _presentationId = presentationId;
+
+ Widget build(BuildContext context, AppState appState, AppActions appActions) {
+ if (!appState.decks.containsKey(_deckId)) {
+ // TODO(aghassemi): Proper error page with navigation back to main view.
+ return new Text('Deck no longer exists.');
+ }
+
+ var deckState = appState.decks[_deckId];
+
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)));
+ floatingActionButton:
+ _buildSyncUpNavigationFab(context, appState, appActions),
+ body: new Material(
+ child: new SlideShow(appActions, deckState,
+ appState.presentations[_presentationId])));
+ }
+
+ _buildSyncUpNavigationFab(
+ BuildContext context, AppState appState, AppActions appActions) {
+ if (_presentationId == null) {
+ // Not in a presentation.
+ return null;
+ }
+
+ PresentationState presentationState =
+ appState.presentations[_presentationId];
+
+ // If not navigating out of sync, do not show the sync icon.
+ if (presentationState == null || !presentationState.isNavigationOutOfSync) {
+ return null;
+ }
+
+ return new FloatingActionButton(child: new Icon(icon: 'notification/sync'),
+ onPressed: () async {
+ appActions.syncUpNavigationWithPresentation(_deckId, _presentationId);
+ });
}
}
-class SlideShow extends StatefulComponent {
- final String deckId;
- SlideShow(this.deckId);
+class SlideShow extends StatelessComponent {
+ AppActions _appActions;
+ DeckState _deckState;
+ PresentationState _presentationState;
- _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();
- }
+ SlideShow(this._appActions, this._deckState, this._presentationState);
Widget build(BuildContext context) {
- if (_slides == null) {
- // TODO(aghassemi): Remove when store operations become sync.
- return new Text('Loading');
+ if (_deckState.slides.length == 0) {
+ // TODO(aghassemi): Proper error page with navigation back to main view.
+ return new Text('No slide to show.');
}
- var slideData = _slides[_currSlideNum];
+
+ int currSlideNum;
+
+ bool isFollowingPresenter =
+ _presentationState != null && !_presentationState.isNavigationOutOfSync;
+
+ if (isFollowingPresenter) {
+ currSlideNum = _presentationState.currSlideNum;
+ } else {
+ currSlideNum = _deckState.currSlideNum;
+ }
+
+ if (currSlideNum >= _deckState.slides.length) {
+ // TODO(aghassemi): Can this ever happen?
+ // -What if slide number set by another peer is synced before the actual slides?
+ // -What if we have navigated to a particular slide on our own and peer deletes that slide?
+ // I think without careful batching and consuming watch events as batches, this could happen
+ // maybe for a split second until rest of data syncs up.
+ // UI needs to be bullet-roof, a flicker in the UI is better than an exception and crash.
+ log.shout(
+ 'Current slide number $currSlideNum is greater than the number of slides ${_deckState.slides.length}');
+
+ // TODO(aghassemi): Proper error page with navigation back to main view.
+ return new Text('Slide does not exist.');
+ }
+
+ var slideData = _deckState.slides[currSlideNum];
var image = new AsyncImage(
- provider: imageProvider.getSlideImage(
- config.deckId, _currSlideNum, slideData),
+ provider: imageProvider.getSlideImage(_deckState.deck.key, slideData),
fit: ImageFit.contain);
var navWidgets = [
- _buildSlideNav(_currSlideNum - 1),
- _buildSlideNav(_currSlideNum + 1)
+ _buildSlideNav(currSlideNum - 1),
+ _buildSlideNav(currSlideNum + 1)
];
return new Block(
- [image, new Text(_currSlideNum.toString()), new Row(navWidgets)]);
+ [image, new Text(currSlideNum.toString()), new Row(navWidgets)]);
}
Widget _buildSlideNav(int slideNum) {
var card;
- if (slideNum >= 0 && slideNum < _slides.length) {
- card = _buildThumbnailNav(config.deckId, slideNum, _slides[slideNum],
- onTap: () {
- _store.setCurrSlideNum(config.deckId, slideNum);
- });
+ if (slideNum >= 0 && slideNum < _deckState.slides.length) {
+ var onTap = () => _appActions.setCurrSlideNum(
+ _deckState.deck.key, slideNum,
+ presentationId: _presentationState?.key);
+
+ card = _buildThumbnailNav(
+ _deckState.deck.key, slideNum, _deckState.slides[slideNum],
+ onTap: onTap);
} else {
card = new Container(
width: style.Size.thumbnailNavWidth,
@@ -110,7 +139,7 @@
Widget _buildThumbnailNav(String deckId, int slideIndex, model.Slide slideData,
{Function onTap}) {
var thumbnail = new AsyncImage(
- provider: imageProvider.getSlideImage(deckId, slideIndex, slideData),
+ provider: imageProvider.getSlideImage(deckId, slideData),
height: style.Size.thumbnailNavHeight,
fit: ImageFit.cover);
diff --git a/dart/lib/components/syncslides_page.dart b/dart/lib/components/syncslides_page.dart
new file mode 100644
index 0000000..c6c3a25
--- /dev/null
+++ b/dart/lib/components/syncslides_page.dart
@@ -0,0 +1,50 @@
+// 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 '../stores/store.dart';
+
+typedef Widget SyncSlidesPageBuilder(
+ BuildContext context, AppState appState, AppActions appActions);
+
+// The base class for every page.
+// Responsible for watching state changes from the store and passing
+// the state and actions to the build function of descendant components.
+abstract class SyncSlidesPage extends StatefulComponent {
+ build(BuildContext context, AppState appState, AppActions appActions);
+ initState(AppState appState, AppActions appActions) {}
+
+ _SyncSlidesPage createState() => new _SyncSlidesPage();
+}
+
+class _SyncSlidesPage extends State<SyncSlidesPage> {
+ Store _store = new Store.singleton();
+ AppState _state;
+ StreamSubscription _onStateChangeSubscription;
+
+ void _updateState(AppState newState) {
+ setState(() => _state = newState);
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ config.initState(_store.state, _store.actions);
+ _state = _store.state;
+ _onStateChangeSubscription = _store.onStateChange.listen(_updateState);
+ }
+
+ @override
+ void dispose() {
+ _onStateChangeSubscription.cancel();
+ super.dispose();
+ }
+
+ Widget build(BuildContext context) {
+ return config.build(context, _state, _store.actions);
+ }
+}
diff --git a/dart/lib/components/toast.dart b/dart/lib/components/toast.dart
new file mode 100644
index 0000000..488601d
--- /dev/null
+++ b/dart/lib/components/toast.dart
@@ -0,0 +1,42 @@
+// 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:core';
+
+import 'package:flutter/material.dart';
+
+import '../styles/common.dart' as style;
+
+class Durations {
+ static const Duration permanent = const Duration(days: 100);
+ static const Duration long = const Duration(seconds: 5);
+ static const Duration medium = kSnackBarMediumDisplayDuration;
+ static const Duration short = kSnackBarShortDisplayDuration;
+}
+
+ScaffoldFeatureController _currSnackBar;
+
+void info(GlobalKey scaffoldKey, String text,
+ {Duration duration: Durations.short}) {
+ _closePrevious();
+ _currSnackBar = scaffoldKey.currentState
+ .showSnackBar(new SnackBar(content: new Text(text), duration: duration));
+}
+
+void error(GlobalKey scaffoldKey, String text, Error err,
+ {Duration duration: Durations.long}) {
+ _closePrevious();
+ _currSnackBar = scaffoldKey.currentState.showSnackBar(new SnackBar(
+ // TODO(aghassemi): Add "Details" action to error toasts and move error text there.
+ content: new Text(text + ' - ERROR: $err', style: style.Text.error),
+ duration: duration));
+}
+
+void _closePrevious() {
+ // TODO(aghassemi): Fix this in Flutter. Currently close() throws exception
+ // if snackbar is already closed.
+ try {
+ _currSnackBar?.close();
+ } catch (e) {}
+}
diff --git a/dart/lib/config.dart b/dart/lib/config.dart
index fe5bca2..1ba0489 100644
--- a/dart/lib/config.dart
+++ b/dart/lib/config.dart
@@ -3,5 +3,6 @@
// 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;
+
+String mounttableAddr = '/192.168.86.254:8101';
diff --git a/dart/lib/loaders/demo_loader.dart b/dart/lib/loaders/demo_loader.dart
index 52b4257..781dc6a 100644
--- a/dart/lib/loaders/demo_loader.dart
+++ b/dart/lib/loaders/demo_loader.dart
@@ -7,20 +7,15 @@
import '../models/all.dart' as model;
import '../stores/store.dart';
-import '../utils/uuid.dart' as uuidutil;
import '../utils/asset.dart' as assetutil;
-
+import '../utils/uuid.dart' as uuidutil;
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();
+ final Store _store = new Store.singleton();
+ final Random _rand = new Random();
static final List<String> thumbnails = [
'assets/images/sample_decks/baku/thumb.png',
@@ -67,15 +62,17 @@
var numSlides = _rand.nextInt(maxNumSlides);
for (var i = 0; i < numSlides; i++) {
var slideIndex = i % slides.length;
- yield new model.Slide(await assetutil.getRawBytes(
- 'assets/images/sample_decks/vanadium/${slideIndex + 1}.jpg'));
+ yield new model.Slide(
+ i,
+ await assetutil.getRawBytes(
+ 'assets/images/sample_decks/vanadium/${slideIndex + 1}.jpg'));
}
}
- Future addDeck() async {
+ Future loadDeck() async {
var deck = await _getRandomDeck();
List<model.Slide> slides = await _getRandomSlides().toList();
- await _store.addDeck(deck);
- await _store.setSlides(deck.key, slides);
+ await _store.actions.addDeck(deck);
+ await _store.actions.setSlides(deck.key, slides);
}
}
diff --git a/dart/lib/loaders/loader_factory.dart b/dart/lib/loaders/factory.dart
similarity index 66%
rename from dart/lib/loaders/loader_factory.dart
rename to dart/lib/loaders/factory.dart
index 7b006f1..354dd93 100644
--- a/dart/lib/loaders/loader_factory.dart
+++ b/dart/lib/loaders/factory.dart
@@ -2,17 +2,15 @@
// 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 '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();
- }
+Loader createDemoLoader() {
+ return new DemoLoader();
+}
+
+Loader createSdCardLoader() {
+ return new SdCardLoader();
}
diff --git a/dart/lib/loaders/loader.dart b/dart/lib/loaders/loader.dart
index c206b36..8113924 100644
--- a/dart/lib/loaders/loader.dart
+++ b/dart/lib/loaders/loader.dart
@@ -4,15 +4,17 @@
import 'dart:async';
-import 'loader_factory.dart' as loaderFactory;
+import 'factory.dart' as factory;
// Loader is responsible for importing existing decks and slides into the store.
abstract class Loader {
- static Loader _singletonLoader = loaderFactory.create();
-
- factory Loader.singleton() {
- return _singletonLoader;
+ factory Loader.demo() {
+ return factory.createDemoLoader();
}
- Future addDeck();
+ factory Loader.sdcard() {
+ return factory.createSdCardLoader();
+ }
+
+ Future loadDeck();
}
diff --git a/dart/lib/loaders/sdcard_loader.dart b/dart/lib/loaders/sdcard_loader.dart
index a2b0102..60aa269 100644
--- a/dart/lib/loaders/sdcard_loader.dart
+++ b/dart/lib/loaders/sdcard_loader.dart
@@ -7,7 +7,7 @@
import 'loader.dart';
class SdCardLoader implements Loader {
- Future addDeck() {
+ Future loadDeck() {
throw new UnimplementedError();
}
}
diff --git a/dart/lib/models/slide.dart b/dart/lib/models/slide.dart
index ebb665a..e93b03a 100644
--- a/dart/lib/models/slide.dart
+++ b/dart/lib/models/slide.dart
@@ -4,20 +4,25 @@
import 'dart:convert';
-// Slide represents an independent slide without ties to a specific deck.
+// Slide represents a slide within a deck.
class Slide {
+ int _num;
+ int get num => _num;
+
List<int> _image;
List<int> get image => _image;
- Slide(this._image) {}
+ Slide(this._num, this._image) {}
Slide.fromJson(String json) {
Map map = JSON.decode(json);
+ _num = map['num'];
_image = map['image'];
}
String toJson() {
Map map = new Map();
+ map['num'] = _num;
map['image'] = image;
return JSON.encode(map);
}
diff --git a/dart/lib/stores/actions.dart b/dart/lib/stores/actions.dart
new file mode 100644
index 0000000..093bab8
--- /dev/null
+++ b/dart/lib/stores/actions.dart
@@ -0,0 +1,48 @@
+// 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.
+
+part of store;
+
+// Defines all possible actions that the application can perform.
+abstract class AppActions {
+ //////////////////////////////////////
+ // Decks
+
+ // Adds a new deck.
+ Future addDeck(model.Deck deck);
+
+ // Removes a deck given its key.
+ Future removeDeck(String key);
+
+ // Loads a demo deck.
+ Future loadDemoDeck();
+
+ // Loads a deck from SdCard.
+ Future loadDeckFromSdCard();
+
+ // Sets the slides for a deck.
+ Future setSlides(String deckKey, List<model.Slide> slides);
+
+ // Sets the current slide number for a deck.
+ Future setCurrSlideNum(String deckId, int slideNum, {String presentationId});
+
+ //////////////////////////////////////
+ // Presentation
+
+ // Joins an advertised presentation.
+ Future joinPresentation(model.PresentationAdvertisement presentation);
+
+ // Starts a presentation.
+ Future<model.PresentationAdvertisement> startPresentation(String deckId);
+
+ // Stops the given presentation.
+ Future stopPresentation(String presentationId);
+
+ // Stops all presentations.
+ Future stopAllPresentations();
+
+ // If viewer has started navigating on their own, this will sync the navigation
+ // back up with the presentation.
+ Future syncUpNavigationWithPresentation(String deckId, String presentationId);
+}
diff --git a/dart/lib/stores/keyutil.dart b/dart/lib/stores/keyutil.dart
deleted file mode 100644
index eb543c8..0000000
--- a/dart/lib/stores/keyutil.dart
+++ /dev/null
@@ -1,42 +0,0 @@
-// 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 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
deleted file mode 100644
index bfee15f..0000000
--- a/dart/lib/stores/memory_store.dart
+++ /dev/null
@@ -1,135 +0,0 @@
-// 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 'state.dart';
-import 'store.dart';
-
-// A memory-based implementation of Store.
-class MemoryStore implements Store {
- StreamController _onDecksChangeEmitter;
- Map<String, String> _decksMap;
- Map<String, String> _slidesMap;
- Map<String, int> _currSlideNumMap;
- Map<String, StreamController> _currSlideNumChangeEmitterMap;
- State _state = new State();
- StreamController _stateChangeEmitter = new StreamController.broadcast();
-
- MemoryStore()
- : _onDecksChangeEmitter = new StreamController.broadcast(),
- _decksMap = new Map(),
- _slidesMap = new Map(),
- _currSlideNumMap = new Map(),
- _currSlideNumChangeEmitterMap = new Map(),
- _state = new State(),
- _stateChangeEmitter = new StreamController.broadcast();
-
- //////////////////////////////////////
- // State
-
- State get state => _state;
- Stream get onStateChange => _stateChangeEmitter.stream;
-
- //////////////////////////////////////
- // Decks
-
- 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<model.Deck> getDeck(String key) async {
- return new model.Deck.fromJson(key, _decksMap[key]);
- }
-
- Future addDeck(model.Deck deck) async {
- var json = deck.toJson();
- _decksMap[deck.key] = json;
- getAllDecks().then(_onDecksChangeEmitter.add);
- }
-
- Future removeDeck(String deckKey) async {
- _decksMap.remove(deckKey);
- _slidesMap.keys
- .where((slideKey) =>
- slideKey.startsWith(keyutil.getDeckKeyPrefix(deckKey)))
- .toList()
- .forEach(_slidesMap.remove);
- getAllDecks().then(_onDecksChangeEmitter.add);
- }
-
- Stream<List<model.Deck>> get onDecksChange => _onDecksChangeEmitter.stream;
-
- //////////////////////////////////////
- // Slides
-
- 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];
- }
- }
-
- //////////////////////////////////////
- // 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];
- }
-
- //////////////////////////////////////
- // Presentation
-
- static final Error noPresentationSupportError =
- new UnsupportedError('MemoryStore does not support presentation.');
-
- Future<model.PresentationAdvertisement> startPresentation(String deckId) {
- throw noPresentationSupportError;
- }
-
- Future stopPresentation(String presentationId) {
- throw noPresentationSupportError;
- }
-
- Future stopAllPresentations() {
- throw noPresentationSupportError;
- }
-}
diff --git a/dart/lib/stores/state.dart b/dart/lib/stores/state.dart
index 3ecf6bd..a3afb59 100644
--- a/dart/lib/stores/state.dart
+++ b/dart/lib/stores/state.dart
@@ -2,18 +2,29 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-import '../models/all.dart' as model;
+part of store;
-// Represents current state of the application.
-class State {
- // TODO(aghassemi): The new store model is to have one state object
- // and a change event instead of async getters.
- // This model has not been implemented for decks and slides yet but
- // we are using the new model for presentation advertisements.
+// Represents the current state of the data that the application holds.
+// Application is rendered purely based on this state.
+// State is deeply-immutable outside of store code.
+abstract class AppState {
+ UnmodifiableMapView<String, DeckState> get decks;
+ UnmodifiableMapView<String, PresentationState> get presentations;
+ UnmodifiableListView<
+ model.PresentationAdvertisement> get advertisedPresentations;
+ UnmodifiableMapView<String,
+ model.PresentationAdvertisement> get presentationAdvertisements;
+}
- // TODO(aghassemi): State needs to be deeply immutable.
- // Maybe https://github.com/google/built_value.dart can help?
- List<model.PresentationAdvertisement> livePresentations;
+abstract class DeckState {
+ model.Deck get deck;
+ UnmodifiableListView<model.Slide> get slides;
+ int get currSlideNum;
+}
- State() : livePresentations = new List();
+abstract class PresentationState {
+ String get key;
+ int get currSlideNum;
+ bool get isDriving;
+ bool get isNavigationOutOfSync;
}
diff --git a/dart/lib/stores/store.dart b/dart/lib/stores/store.dart
index 6c817c8..da994d5 100644
--- a/dart/lib/stores/store.dart
+++ b/dart/lib/stores/store.dart
@@ -2,70 +2,26 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
+library store;
+
import 'dart:async';
+import 'dart:collection';
import '../models/all.dart' as model;
+import 'utils/factory.dart' as factory;
-import 'state.dart';
-import 'store_factory.dart' as storeFactory;
+part 'actions.dart';
+part 'state.dart';
-// Provides APIs for reading and writing app-related data.
+// Provides the state, actions and state change event to the application.
abstract class Store {
- static Store _singletonStore = storeFactory.create();
+ static Store _singletonStore = factory.create();
factory Store.singleton() {
return _singletonStore;
}
- //////////////////////////////////////
- // State
-
- State get state;
+ AppActions get actions;
+ AppState get state;
Stream get onStateChange;
-
- //////////////////////////////////////
- // Decks
-
- // Returns all the existing decks.
- Future<List<model.Deck>> getAllDecks();
-
- // Returns the deck for the given key.
- Future<model.Deck> getDeck(String key);
-
- // Adds a new deck.
- Future addDeck(model.Deck deck);
-
- // Removes 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);
-
- //////////////////////////////////////
- // Slideshow
-
- Future<int> getCurrSlideNum(String deckId);
-
- Future setCurrSlideNum(String deckId, int slideNum);
-
- Stream<int> onCurrSlideNumChange(String deckId);
-
- //////////////////////////////////////
- // Presentation
-
- Future<model.PresentationAdvertisement> startPresentation(String deckId);
-
- Future stopPresentation(String presentationId);
-
- Future stopAllPresentations();
}
diff --git a/dart/lib/stores/store_factory.dart b/dart/lib/stores/store_factory.dart
deleted file mode 100644
index c8cc53e..0000000
--- a/dart/lib/stores/store_factory.dart
+++ /dev/null
@@ -1,18 +0,0 @@
-// 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/actions.dart b/dart/lib/stores/syncbase/actions.dart
new file mode 100644
index 0000000..2fe3428
--- /dev/null
+++ b/dart/lib/stores/syncbase/actions.dart
@@ -0,0 +1,205 @@
+// 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.
+
+part of syncbase_store;
+
+class _AppActions extends AppActions {
+ _AppState _state;
+ Function _emitChange;
+ _AppActions(this._state, this._emitChange);
+
+ //////////////////////////////////////
+ // Decks
+
+ Future addDeck(model.Deck deck) async {
+ log.info("Adding deck ${deck.name}...");
+ sb.SyncbaseTable tb = await _getDecksTable();
+ await tb.put(deck.key, UTF8.encode(deck.toJson()));
+ log.info("Deck ${deck.name} added.");
+ }
+
+ Future removeDeck(String deckKey) async {
+ sb.SyncbaseTable tb = await _getDecksTable();
+ tb.deleteRange(new sb.RowRange.prefix(deckKey));
+ }
+
+ Future setSlides(String deckKey, List<model.Slide> slides) async {
+ sb.SyncbaseTable tb = await _getDecksTable();
+
+ slides.forEach((slide) async {
+ // TODO(aghassemi): Use batching.
+ await tb.put(
+ keyutil.getSlideKey(deckKey, slide.num), UTF8.encode(slide.toJson()));
+ });
+ }
+
+ Future setCurrSlideNum(String deckId, int slideNum,
+ {String presentationId}) async {
+ var deckState = _state._getOrCreateDeckState(deckId);
+ if (slideNum < 0 || slideNum >= deckState.slides.length) {
+ throw new ArgumentError.value(slideNum,
+ "Slide number out of range. Only ${deckState.slides.length} slides exist.");
+ }
+
+ // TODO(aghassemi): Have a session table to persist UI state such as
+ // local slide number within a deck, whether user is navigating a presentation
+ // on their own, and similar UI state that is desirable to be persisted.
+ deckState._currSlideNum = slideNum;
+
+ // Is slide number change happening within a presentation?
+ if (presentationId != null) {
+ if (!_state.presentations.containsKey(presentationId)) {
+ throw new ArgumentError.value(
+ presentationId, "Presentation does not exist.");
+ }
+
+ _PresentationState presentationState =
+ _state.presentations[presentationId];
+
+ // Is the current user driving the presentation?
+ if (presentationState.isDriving) {
+ // Update the common slide number for the presentation.
+ sb.SyncbaseTable tb = await _getPresentationsTable();
+ await tb.put(
+ keyutil.getPresentationCurrSlideNumKey(deckId, presentationId),
+ [slideNum]);
+ } else {
+ // User is not driving the presentation so they are navigating on their own.
+ presentationState._isNavigationOutOfSync = true;
+ }
+ }
+ _emitChange();
+ }
+
+ Future loadDemoDeck() {
+ return new Loader.demo().loadDeck();
+ }
+
+ Future loadDeckFromSdCard() {
+ return new Loader.demo().loadDeck();
+ }
+
+ //////////////////////////////////////
+ // Presentation
+
+ Future<model.PresentationAdvertisement> startPresentation(
+ String deckId) async {
+ var alreadyAdvertised = _state._advertisedPresentations
+ .any((model.PresentationAdvertisement p) => p.deck.key == deckId);
+ if (alreadyAdvertised) {
+ throw new ArgumentError.value(deckId,
+ 'Cannot simultaneously present the same deck. Presentation already in progress.');
+ }
+
+ if (!_state._decks.containsKey(deckId)) {
+ throw new ArgumentError.value(deckId, 'Deck no longer exists.');
+ }
+
+ String uuid = uuidutil.createUuid();
+ String syncgroupName = keyutil.getPresentationSyncgroupName(uuid);
+
+ model.Deck deck = _state._getOrCreateDeckState(deckId)._deck;
+ var presentation =
+ new model.PresentationAdvertisement(uuid, deck, syncgroupName);
+
+ await sb.createSyncgroup(syncgroupName, [
+ sb.SyncbaseClient.syncgroupPrefix(decksTableName, deckId),
+ sb.SyncbaseClient.syncgroupPrefix(presentationsTableName, deckId)
+ ]);
+
+ await discovery.advertise(presentation);
+ _state._advertisedPresentations.add(presentation);
+
+ await joinPresentation(presentation);
+
+ return presentation;
+ }
+
+ Future joinPresentation(model.PresentationAdvertisement presentation) async {
+ bool isMyOwnPresentation =
+ _state._advertisedPresentations.any((p) => p.key == presentation.key);
+
+ if (!isMyOwnPresentation) {
+ await sb.joinSyncgroup(presentation.syncgroupName);
+ String deckId = presentation.deck.key;
+ Completer completer = new Completer();
+
+ // Wait until at least the slide for current page number is synced.
+ new Timer.periodic(new Duration(milliseconds: 30), (Timer timer) {
+ if (_state._decks.containsKey(deckId) &&
+ _state._decks[deckId].deck != null &&
+ _state._decks[deckId].slides.length >
+ _state._decks[deckId].currSlideNum &&
+ !completer.isCompleted) {
+ timer.cancel();
+ completer.complete();
+ }
+ });
+ await completer.future.timeout(new Duration(seconds: 20));
+ }
+
+ _PresentationState presentationState =
+ _state._getOrCreatePresentationState(presentation.key);
+
+ // TODO(aghassemi): For now, only the presenter can drive. Later when we have
+ // identity and delegation support, this will change to: if "driver == me".
+ presentationState._isDriving = isMyOwnPresentation;
+
+ log.info('Joined presentation ${presentation.key}');
+ }
+
+ Future stopPresentation(String presentationId) async {
+ await discovery.stopAdvertising(presentationId);
+ _state._advertisedPresentations.removeWhere((p) => p.key == presentationId);
+ _state._presentations.remove(presentationId);
+ log.info('Presentation $presentationId stopped');
+ }
+
+ Future stopAllPresentations() async {
+ // Stop all presentations in parallel.
+ return Future.wait(_state._advertisedPresentations
+ .map((model.PresentationAdvertisement p) {
+ return stopPresentation(p.key);
+ }));
+ }
+
+ Future syncUpNavigationWithPresentation(
+ String deckId, String presentationId) async {
+ if (!_state.presentations.containsKey(presentationId)) {
+ throw new ArgumentError.value(
+ presentationId, "Presentation does not exist.");
+ }
+
+ _PresentationState presentationState = _state.presentations[presentationId];
+
+ // Set the current slide number to the presentation's current slide number.
+ await setCurrSlideNum(deckId, presentationState.currSlideNum);
+
+ presentationState._isNavigationOutOfSync = false;
+ }
+}
+
+//////////////////////////////////////
+// Utilities
+
+Future<sb.SyncbaseTable> _getTable(String tableName) async {
+ sb.SyncbaseDatabase sbDb = await sb.getDatabase();
+ sb.SyncbaseTable tb = sbDb.table(tableName);
+ try {
+ await tb.create(sb.createOpenPerms());
+ } catch (e) {
+ if (!errorsutil.isExistsError(e)) {
+ throw e;
+ }
+ }
+ return tb;
+}
+
+Future<sb.SyncbaseTable> _getDecksTable() {
+ return _getTable(decksTableName);
+}
+
+Future<sb.SyncbaseTable> _getPresentationsTable() {
+ return _getTable(presentationsTableName);
+}
diff --git a/dart/lib/stores/syncbase/consts.dart b/dart/lib/stores/syncbase/consts.dart
new file mode 100644
index 0000000..0e4955a
--- /dev/null
+++ b/dart/lib/stores/syncbase/consts.dart
@@ -0,0 +1,9 @@
+// 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.
+
+part of syncbase_store;
+
+const String decksTableName = 'decks';
+const String presentationsTableName = 'presentations';
+final Logger log = new Logger('store/syncbase_store');
diff --git a/dart/lib/stores/syncbase/state.dart b/dart/lib/stores/syncbase/state.dart
new file mode 100644
index 0000000..f2206b6
--- /dev/null
+++ b/dart/lib/stores/syncbase/state.dart
@@ -0,0 +1,69 @@
+// 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.
+
+part of syncbase_store;
+
+class _AppState implements AppState {
+ List<model.PresentationAdvertisement> advertisedPresentations;
+ Map<String, model.PresentationAdvertisement> presentationAdvertisements;
+ Map<String, DeckState> decks;
+ Map<String, PresentationState> presentations;
+
+ _AppState() {
+ presentationAdvertisements =
+ new UnmodifiableMapView(_presentationsAdvertisements);
+ decks = new UnmodifiableMapView(_decks);
+ presentations = new UnmodifiableMapView(_presentations);
+ advertisedPresentations =
+ new UnmodifiableListView(_advertisedPresentations);
+ }
+
+ Map<String, model.PresentationAdvertisement> _presentationsAdvertisements =
+ new Map();
+ Map<String, _DeckState> _decks = new Map();
+ Map<String, _PresentationState> _presentations = new Map();
+ List<model.PresentationAdvertisement> _advertisedPresentations = new List();
+
+ _DeckState _getOrCreateDeckState(String deckId) {
+ return _decks.putIfAbsent(deckId, () {
+ return new _DeckState();
+ });
+ }
+
+ _PresentationState _getOrCreatePresentationState(String presentationId) {
+ return _presentations.putIfAbsent(presentationId, () {
+ return new _PresentationState(presentationId);
+ });
+ }
+}
+
+class _DeckState implements DeckState {
+ model.Deck _deck;
+ model.Deck get deck => _deck;
+
+ List<model.Slide> _slides = new List();
+ List<model.Slide> slides;
+
+ int _currSlideNum = 0;
+ int get currSlideNum => _currSlideNum;
+
+ _DeckState() {
+ slides = new UnmodifiableListView(_slides);
+ }
+}
+
+class _PresentationState implements PresentationState {
+ final String key;
+
+ int _currSlideNum = 0;
+ int get currSlideNum => _currSlideNum;
+
+ bool _isDriving = false;
+ bool get isDriving => _isDriving;
+
+ bool _isNavigationOutOfSync = false;
+ bool get isNavigationOutOfSync => _isNavigationOutOfSync;
+
+ _PresentationState(this.key);
+}
diff --git a/dart/lib/stores/syncbase/store.dart b/dart/lib/stores/syncbase/store.dart
new file mode 100644
index 0000000..4095383
--- /dev/null
+++ b/dart/lib/stores/syncbase/store.dart
@@ -0,0 +1,168 @@
+// 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.
+
+library syncbase_store;
+
+import 'dart:async';
+import 'dart:collection';
+import 'dart:convert';
+
+import 'package:logging/logging.dart';
+
+import '../../discovery/client.dart' as discovery;
+import '../../loaders/loader.dart';
+import '../../models/all.dart' as model;
+import '../../syncbase/client.dart' as sb;
+import '../../utils/errors.dart' as errorsutil;
+import '../../utils/uuid.dart' as uuidutil;
+import '../store.dart';
+import '../utils/key.dart' as keyutil;
+
+part 'actions.dart';
+part 'state.dart';
+part 'consts.dart';
+
+// Implementation of Store using Syncbase (http://v.io/syncbase) storage system.
+class SyncbaseStore implements Store {
+ _AppState _state;
+ _AppActions _actions;
+ StreamController _stateChangeEmitter = new StreamController.broadcast();
+
+ AppState get state => _state;
+ Stream get onStateChange => _stateChangeEmitter.stream;
+ AppActions get actions => _actions;
+
+ SyncbaseStore() {
+ _state = new _AppState();
+ _actions = new _AppActions(_state, _triggerStateChange);
+
+ sb.getDatabase().then((sb.SyncbaseDatabase db) async {
+ // Make sure all table exists.
+ await _ensureTablesExist();
+
+ // TODO(aghassemi): Use the multi-table scan and watch API when ready.
+ // See https://github.com/vanadium/issues/issues/923
+ for (String table in [decksTableName, presentationsTableName]) {
+ _getInitialValuesAndStartWatching(db, table);
+ }
+ _startScanningForPresentations();
+ });
+ }
+
+ // Note(aghassemi): We could have copied the state to provide a snapshot at the time of
+ // change, however for this application we did not choose to do so because:
+ // 1- The state is already publically deeply-immutable, so only store code can mutate it.
+ // 2- Although there is a chance by the time UI rerenders due to a change event, the state
+ // may have changed causing UI to skip rendering the intermediate states, it is not an
+ // undesirable behaviour for SyncSlides. This behaviour may not be acceptable for different
+ // categories of apps that require continuity of rendering, such as games.
+ void _triggerStateChange() => _stateChangeEmitter.add(_state);
+
+ Future _startScanningForPresentations() async {
+ discovery.onFound.listen((model.PresentationAdvertisement newP) {
+ _state._presentationsAdvertisements[newP.key] = newP;
+ _triggerStateChange();
+ });
+
+ discovery.onLost.listen((String pId) {
+ _state._presentationsAdvertisements.remove(pId);
+ _triggerStateChange();
+ });
+
+ discovery.startScan();
+ }
+
+ Future _getInitialValuesAndStartWatching(
+ sb.SyncbaseDatabase sbDb, String table) async {
+ // TODO(aghassemi): Ideally we wouldn't need an initial query and can configure
+ // watch to give both initial values and future changes.
+ // See https://github.com/vanadium/issues/issues/917
+ var batchDb =
+ await sbDb.beginBatch(sb.SyncbaseClient.batchOptions(readOnly: true));
+ var resumeMarker = await batchDb.getResumeMarker();
+
+ // Get initial values in a batch.
+ String query = 'SELECT k, v FROM $table';
+ Stream<sb.Result> results = batchDb.exec(query);
+ // NOTE(aghassemi): First row is always the name of the columns, so we skip(1).
+ results.skip(1).forEach((sb.Result result) => _onChange(
+ table,
+ sb.WatchChangeTypes.put,
+ UTF8.decode(result.values[0]),
+ result.values[1]));
+
+ await batchDb.abort();
+
+ // Start watching from batch's resume marker.
+ var stream = sbDb.watch(table, '', resumeMarker);
+ stream.listen((sb.WatchChange change) =>
+ _onChange(table, change.changeType, change.rowKey, change.valueBytes));
+ }
+
+ _onChange(String table, int changeType, String rowKey, List<int> value) {
+ log.finest('Change in $table for $rowKey of the type $changeType.');
+
+ keyutil.KeyType keyType = keyutil.getKeyType(rowKey);
+ switch (keyType) {
+ case keyutil.KeyType.Deck:
+ _onDeckChange(changeType, rowKey, value);
+ break;
+ case keyutil.KeyType.Slide:
+ _onSlideChange(changeType, rowKey, value);
+ break;
+ case keyutil.KeyType.PresentationCurrSlideNum:
+ _onPresentationSlideNumChange(changeType, rowKey, value);
+ break;
+ case keyutil.KeyType.Unknown:
+ log.severe('Got change for $rowKey with an unknown key type.');
+ }
+ _triggerStateChange();
+ }
+
+ _onDeckChange(int changeType, String rowKey, List<int> value) {
+ var deckId = rowKey;
+ if (changeType == sb.WatchChangeTypes.put) {
+ _state._getOrCreateDeckState(deckId)._deck =
+ new model.Deck.fromJson(deckId, UTF8.decode(value));
+ } else if (changeType == sb.WatchChangeTypes.delete) {
+ _state.decks.remove(deckId);
+ }
+ }
+
+ _onSlideChange(int changeType, String rowKey, List<int> value) {
+ var deckId = keyutil.currSlideKeyToDeckId(rowKey);
+ var index = keyutil.currSlideKeyToIndex(rowKey);
+ var slides = _state._getOrCreateDeckState(deckId)._slides;
+ if (changeType == sb.WatchChangeTypes.put) {
+ var slide = new model.Slide.fromJson(UTF8.decode(value));
+ slides.add(slide);
+ } else if (changeType == sb.WatchChangeTypes.delete) {
+ slides.removeWhere((s) => s.num == index);
+ }
+
+ // Keep the slides sorted by number.
+ slides.sort((model.Slide a, model.Slide b) {
+ return a.num.compareTo(b.num);
+ });
+ }
+
+ _onPresentationSlideNumChange(
+ int changeType, String rowKey, List<int> value) {
+ var presentationId =
+ keyutil.presentationCurrSlideNumKeyToPresentationId(rowKey);
+ var presentationState =
+ _state._getOrCreatePresentationState(presentationId);
+ if (changeType == sb.WatchChangeTypes.put) {
+ int currSlideNum = value[0];
+ presentationState._currSlideNum = currSlideNum;
+ } else {
+ presentationState._currSlideNum = 0;
+ }
+ }
+
+ Future _ensureTablesExist() async {
+ await _getDecksTable();
+ await _getPresentationsTable();
+ }
+}
diff --git a/dart/lib/stores/syncbase_store.dart b/dart/lib/stores/syncbase_store.dart
deleted file mode 100644
index e6d6ddf..0000000
--- a/dart/lib/stores/syncbase_store.dart
+++ /dev/null
@@ -1,247 +0,0 @@
-// 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 '../discovery/client.dart' as discovery;
-import '../models/all.dart' as model;
-import '../syncbase/client.dart' as sb;
-import '../utils/errors.dart' as errorsutil;
-import '../utils/uuid.dart' as uuidutil;
-
-import 'keyutil.dart' as keyutil;
-import 'state.dart';
-import 'store.dart';
-
-const String decksTableName = 'Decks';
-
-// Implementation of using Syncbase (http://v.io/syncbase) storage system.
-class SyncbaseStore implements Store {
- StreamController _deckChangeEmitter;
- Map<String, StreamController> _currSlideNumChangeEmitterMap;
- List<model.PresentationAdvertisement> _advertisedPresentations;
- State _state = new State();
- StreamController _stateChangeEmitter = new StreamController.broadcast();
-
- SyncbaseStore() {
- _deckChangeEmitter = new StreamController.broadcast();
- _currSlideNumChangeEmitterMap = new Map();
- _advertisedPresentations = new List();
- _state = new State();
- _stateChangeEmitter = new StreamController.broadcast();
- sb.getDatabase().then((db) {
- _startDecksWatch(db);
- _startScanningForPresentations();
- });
- }
-
- //////////////////////////////////////
- // State
-
- State get state => _state;
- Stream get onStateChange => _stateChangeEmitter.stream;
- void _triggerStateChange() => _stateChangeEmitter.add(_state);
-
- //////////////////////////////////////
- // Decks
-
- 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();
- }
-
- // Return the deck for the given key or null if it does not exist.
- Future<model.Deck> getDeck(String key) async {
- sb.SyncbaseTable tb = await _getDecksTable();
- var value = await tb.get(key);
- return new model.Deck.fromJson(key, UTF8.decode(value));
- }
-
- 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 => _deckChangeEmitter.stream;
-
- model.Deck _toDeck(List<List<int>> row) {
- var key = UTF8.decode(row[0]);
- var value = UTF8.decode(row[1]);
- return new model.Deck.fromJson(key, value);
- }
-
- Future<sb.SyncbaseTable> _getDecksTable() async {
- sb.SyncbaseNoSqlDatabase sbDb = await sb.getDatabase();
- sb.SyncbaseTable tb = sbDb.table(decksTableName);
- try {
- await tb.create(sb.createOpenPerms());
- } catch (e) {
- if (!errorsutil.isExistsError(e)) {
- throw e;
- }
- }
- return tb;
- }
-
- Future _startDecksWatch(sb.SyncbaseNoSqlDatabase sbDb) async {
- var resumeMarker = await sbDb.getResumeMarker();
- var stream = sbDb.watch(decksTableName, '', resumeMarker);
-
- 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.
- 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);
- }
- }
- }
- });
- }
-
- //////////////////////////////////////
- // Slides
-
- 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.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();
- }
-
- 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()));
- }
- }
-
- 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();
- String key = keyutil.getCurrSlideNumKey(deckId);
- // TODO(aghassemi): Run exist and get in a batch.
- if (await tb.row(key).exists()) {
- return 0;
- }
- var v = await tb.get(key);
- 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];
- }
-
- //////////////////////////////////////
- // Presentation
-
- Future<model.PresentationAdvertisement> startPresentation(
- String deckId) async {
- var alreadyAdvertised =
- _advertisedPresentations.any((p) => p.deck.key == deckId);
- if (alreadyAdvertised) {
- throw new ArgumentError(
- 'Cannot simultaneously present the same deck. Presentation already in progress for $deckId.');
- }
-
- model.Deck deck = await this.getDeck(deckId);
- String uuid = uuidutil.createUuid();
- String syncgroupName = '';
- var presentation =
- new model.PresentationAdvertisement(uuid, deck, syncgroupName);
-
- await discovery.advertise(presentation);
- _advertisedPresentations.add(presentation);
-
- return presentation;
- }
-
- Future stopPresentation(String presentationId) async {
- await discovery.stopAdvertising(presentationId);
- _advertisedPresentations.removeWhere((p) => p.key == presentationId);
- }
-
- Future stopAllPresentations() async {
- // Stop all presentations in parallel.
- return Future.wait(
- _advertisedPresentations.map((model.PresentationAdvertisement p) {
- return stopPresentation(p.key);
- }));
- }
-
- Future _startScanningForPresentations() async {
- discovery.onFound.listen((model.PresentationAdvertisement newP) {
- state.livePresentations.add(newP);
- _triggerStateChange();
- });
-
- discovery.onLost.listen((String pId) {
- state.livePresentations.removeWhere((p) => p.key == pId);
- _triggerStateChange();
- });
-
- discovery.startScan();
- }
-}
diff --git a/dart/lib/stores/utils/factory.dart b/dart/lib/stores/utils/factory.dart
new file mode 100644
index 0000000..b9d4122
--- /dev/null
+++ b/dart/lib/stores/utils/factory.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.
+
+import '../store.dart';
+import '../syncbase/store.dart';
+
+// Factory method to create a concrete store instance.
+Store create() {
+ return new SyncbaseStore();
+}
diff --git a/dart/lib/stores/utils/key.dart b/dart/lib/stores/utils/key.dart
new file mode 100644
index 0000000..033368d
--- /dev/null
+++ b/dart/lib/stores/utils/key.dart
@@ -0,0 +1,95 @@
+// 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;
+
+enum KeyType { Deck, Slide, PresentationCurrSlideNum, Unknown }
+
+KeyType getKeyType(String key) {
+ if (isDeckKey(key)) {
+ return KeyType.Deck;
+ } else if (isSlideKey(key)) {
+ return KeyType.Slide;
+ } else if (isPresentationCurrSlideNumKey(key)) {
+ return KeyType.PresentationCurrSlideNum;
+ } else {
+ return KeyType.Unknown;
+ }
+}
+
+// Constructs a slide key.
+String getSlideKey(String deckId, int slideIndex) {
+ return '$deckId/slides/$slideIndex';
+}
+
+// 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('/');
+}
+
+// Returns true if a key is for a slide.
+bool isSlideKey(String key) {
+ return key.contains('/slides/');
+}
+
+// Gets the deck id given a slide key.
+String currSlideKeyToDeckId(String key) {
+ if ((!isSlideKey(key))) {
+ throw new ArgumentError("$key is not a valid slide key.");
+ }
+ return key.substring(0, key.indexOf('/slides/'));
+}
+
+// Gets the slide index given a slide key.
+int currSlideKeyToIndex(String key) {
+ if ((!isSlideKey(key))) {
+ throw new ArgumentError("$key is not a valid slide key.");
+ }
+ var indexStr = key.substring(key.lastIndexOf('/') + 1);
+ return int.parse(indexStr);
+}
+
+// TODO(aghassemi): Don't use regex, just regular split should be fine.
+const String _uuidPattern = '[a-zA-Z0-9-]+';
+final RegExp _currPresentationSlideNumPattern =
+ new RegExp('(?:$_uuidPattern/)($_uuidPattern)(?:/currentslide)');
+
+// Constructs a current slide number key.
+String getPresentationCurrSlideNumKey(String deckId, String presentationId) {
+ return '$deckId/$presentationId/currentslide';
+}
+
+// Gets the presentation id given a current slide number key.
+String presentationCurrSlideNumKeyToPresentationId(String currSlideNumKey) {
+ if ((!isPresentationCurrSlideNumKey(currSlideNumKey))) {
+ throw new ArgumentError(
+ "$currSlideNumKey is not a valid presentation current slide number key.");
+ }
+ return _currPresentationSlideNumPattern.firstMatch(currSlideNumKey).group(1);
+}
+
+// Returns true if a key is a current slide number key.
+bool isPresentationCurrSlideNumKey(String key) {
+ return _currPresentationSlideNumPattern.hasMatch(key);
+}
+
+// Constructs the Syncgroup name for a presentation.
+String getPresentationSyncgroupName(String presentationId) {
+ // TODO(aghassemi): Currently we are assuming the first device
+ // mounts itself under the name 'syncslides' and then every other device
+ // creates the Syncgroup on the first device.
+ // We should have each device mount itself under a unique name and
+ // to create the Syncgroup on their own instance.
+ return '${config.mounttableAddr}/syncslides/%%sync/$presentationId';
+}
diff --git a/dart/lib/styles/common.dart b/dart/lib/styles/common.dart
index d275af0..ccbef24 100644
--- a/dart/lib/styles/common.dart
+++ b/dart/lib/styles/common.dart
@@ -6,11 +6,13 @@
class Text {
static final Color secondaryTextColor = Colors.grey[500];
+ static final Color errorTextColor = Colors.red[500];
static final TextStyle titleStyle = new TextStyle(fontSize: 18.0);
static final TextStyle subtitleStyle =
new TextStyle(fontSize: 12.0, color: secondaryTextColor);
static final TextStyle liveNow =
new TextStyle(fontSize: 12.0, color: theme.accentColor);
+ static final TextStyle error = new TextStyle(color: errorTextColor);
}
class Size {
diff --git a/dart/lib/syncbase/client.dart b/dart/lib/syncbase/client.dart
index 4a25a22..9424099 100644
--- a/dart/lib/syncbase/client.dart
+++ b/dart/lib/syncbase/client.dart
@@ -5,21 +5,24 @@
import 'dart:async';
import 'package:flutter/services.dart' show shell;
+import 'package:logging/logging.dart';
import 'package:syncbase/syncbase_client.dart';
+import '../config.dart' as config;
import '../utils/errors.dart' as errorsutil;
export 'package:syncbase/syncbase_client.dart';
+final Logger log = new Logger('syncbase/client');
+
const String syncbaseMojoUrl =
'https://syncslides.mojo.v.io/packages/syncbase/mojo_services/android/syncbase_server.mojo';
const appName = 'syncslides';
const dbName = 'syncslides';
-SyncbaseNoSqlDatabase _db;
-
+SyncbaseDatabase _db;
// Returns the database handle for the SyncSlides app.
-Future<SyncbaseNoSqlDatabase> getDatabase() async {
+Future<SyncbaseDatabase> getDatabase() async {
if (_db != null) {
return _db;
}
@@ -33,6 +36,26 @@
return _db;
}
+Future createSyncgroup(String syncgroupName, prefixes) async {
+ SyncbaseDatabase sbDb = await getDatabase();
+ SyncbaseSyncgroup sg = sbDb.syncgroup(syncgroupName);
+ var sgSpec = SyncbaseClient.syncgroupSpec(prefixes,
+ perms: createOpenPerms(), mountTables: [config.mounttableAddr]);
+ var myInfo = SyncbaseClient.syncgroupMemberInfo(syncPriority: 1);
+
+ await sg.create(sgSpec, myInfo);
+ log.info('Created syncgroup $syncgroupName');
+}
+
+Future joinSyncgroup(String syncgroupName) async {
+ SyncbaseDatabase sbDb = await getDatabase();
+ SyncbaseSyncgroup sg = sbDb.syncgroup(syncgroupName);
+ var myInfo = SyncbaseClient.syncgroupMemberInfo(syncPriority: 1);
+
+ await sg.join(myInfo);
+ log.info('Joined syncgroup $syncgroupName');
+}
+
Future<SyncbaseApp> _createApp(SyncbaseClient sbClient) async {
var app = sbClient.app(appName);
try {
@@ -46,7 +69,7 @@
return app;
}
-Future<SyncbaseNoSqlDatabase> _createDb(SyncbaseApp app) async {
+Future<SyncbaseDatabase> _createDb(SyncbaseApp app) async {
var db = app.noSqlDatabase(dbName);
try {
await db.create(createOpenPerms());
diff --git a/dart/lib/utils/image_provider.dart b/dart/lib/utils/image_provider.dart
index abaeb9b..a716b07 100644
--- a/dart/lib/utils/image_provider.dart
+++ b/dart/lib/utils/image_provider.dart
@@ -14,8 +14,8 @@
return new _RawImageProvider('thumbnail_${deck.key}', deck.thumbnail);
}
-ImageProvider getSlideImage(String deckId, int slideIndex, model.Slide slide) {
- return new _RawImageProvider('slide_${deckId}_$slideIndex', slide.image);
+ImageProvider getSlideImage(String deckId, model.Slide slide) {
+ return new _RawImageProvider('slide_${deckId}_$slide.num', slide.image);
}
class _RawImageProvider implements ImageProvider {
diff --git a/dart/pubspec.lock b/dart/pubspec.lock
index 49d5397..cfed1e8 100644
--- a/dart/pubspec.lock
+++ b/dart/pubspec.lock
@@ -186,11 +186,11 @@
sky_engine:
description: sky_engine
source: hosted
- version: "0.0.57"
+ version: "0.0.58"
sky_services:
description: sky_services
source: hosted
- version: "0.0.57"
+ version: "0.0.58"
source_map_stack_trace:
description: source_map_stack_trace
source: hosted
@@ -214,7 +214,7 @@
syncbase:
description: syncbase
source: hosted
- version: "0.0.14"
+ version: "0.0.15"
test:
description: test
source: hosted
diff --git a/dart/pubspec.yaml b/dart/pubspec.yaml
index 86d198c..9a6892e 100644
--- a/dart/pubspec.yaml
+++ b/dart/pubspec.yaml
@@ -5,7 +5,7 @@
path: "../../../../../flutter/packages/flutter"
logging: ">=0.11.2 <0.12.0"
mojo_services: ">=0.4.5 <0.5.0"
- syncbase: ">=0.0.9 <0.1.0"
+ syncbase: ">=0.0.15 <0.1.0"
v23discovery: ">=0.0.4 < 0.1.0"
uuid: ">=0.5.0 <0.6.0"
dev_dependencies: