syncslides: Discovery and some UI polish
screenshots: https://goo.gl/photos/SpCoP5JRDVKgcX5F8
-Devices can now discover each other and display
live presentations from other devices.
-Deck and List view Material design polish.
-Demo loader no longer periodically adds/removes decks,
instead the "Add" button loads a random deck.
No sycing yet, next CL will focus on driving and
joining a presentaion.
Change-Id: I58ca3241ddf49215dc2ab002ea9918d44ef62c5c
diff --git a/dart/Makefile b/dart/Makefile
index 88eef82..922d478 100644
--- a/dart/Makefile
+++ b/dart/Makefile
@@ -1,16 +1,20 @@
-ifdef DEVICE_NUM
-
-ifneq ($(DEVICE_NUM), 1)
- REUSE_FLAG := --reuse-server
+ifndef DEVICE_NUM
+ DEVICE_NUM := 1
endif
+ifneq ($(DEVICE_NUM), 1)
+ REUSE_FLAG := --reuse-servers
+endif
+
+ifdef VLOG
+ VLOG_FLAGS = --v=$(VLOG) --logtostderr=true
+endif
+
+SYNCBASE_DATA_DIR=/data/data/org.chromium.mojo.shell/app_home/syncbasedata
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
-
-
+MOUNTTABLE_ADDR := /192.168.86.254:8101
default: run
@@ -33,7 +37,18 @@
# 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 --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)
+ 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)" \
+ --map-origin="https://discovery.mojo.v.io/=$(JIRI_ROOT)/release/mojo/discovery/gen/mojo/android" \
+ --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) \
+ $(VLOG_FLAGS)" \
+ $(DEVICE_FLAG) \
+ $(REUSE_FLAG) \
+ --no-config-file
# Helper targets
run1:
@@ -45,7 +60,18 @@
run4:
DEVICE_NUM=4 make run
+.PHONY: uninstall
+uninstall:
+ adb -s $(DEVICE_ID) uninstall org.chromium.mojo.shell
+
.PHONY: clean
clean:
rm -f app.flx snapshot_blob.bin
rm -rf packages
+
+.PHONY: clean-syncbase
+clean-syncbase:
+ adb -s $(DEVICE_ID) shell run-as org.chromium.mojo.shell rm -rf $(SYNCBASE_DATA_DIR)
+
+.PHONY: very-clean
+.very-clean: clean clean-syncbase
diff --git a/dart/assets/images/defaults/thumbnail.png b/dart/assets/images/defaults/thumbnail.png
new file mode 100644
index 0000000..aaf9a77
--- /dev/null
+++ b/dart/assets/images/defaults/thumbnail.png
Binary files differ
diff --git a/dart/flutter.yaml b/dart/flutter.yaml
index 13c17d3..2da469b 100644
--- a/dart/flutter.yaml
+++ b/dart/flutter.yaml
@@ -1,3 +1,5 @@
name: syncslides
material-design-icons:
- name: navigation/arrow_back
+ - name: navigation/arrow_forward
+ - name: content/add
diff --git a/dart/lib/components/deckgrid.dart b/dart/lib/components/deckgrid.dart
index 410dad5..479e995 100644
--- a/dart/lib/components/deckgrid.dart
+++ b/dart/lib/components/deckgrid.dart
@@ -7,6 +7,7 @@
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;
@@ -15,9 +16,14 @@
// DeckGridPage is the full page view of the list of decks.
class DeckGridPage extends StatelessComponent {
+ Loader _loader = new Loader.singleton();
Widget build(BuildContext context) {
return new Scaffold(
toolBar: new ToolBar(center: new Text('SyncSlides')),
+ floatingActionButton: new FloatingActionButton(
+ child: new Icon(icon: 'content/add'), onPressed: () {
+ _loader.addDeck();
+ }),
body: new Material(child: new DeckGrid()));
}
}
@@ -30,7 +36,8 @@
class _DeckGridState extends State<DeckGrid> {
Store _store = new Store.singleton();
List<model.Deck> _decks = new List<model.Deck>();
- StreamSubscription _onChangeSubscription;
+ StreamSubscription _onDecksChangeSubscription;
+ StreamSubscription _onStateChangeSubscription;
void updateDecks(List<model.Deck> decks) {
setState(() {
@@ -38,54 +45,105 @@
});
}
+ 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.
- _onChangeSubscription = _store.onDecksChange.listen(updateDecks);
+ _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.
- _onChangeSubscription.cancel();
+ _onDecksChangeSubscription.cancel();
+ _onStateChangeSubscription.cancel();
super.dispose();
}
Widget build(BuildContext context) {
- var deckBoxes = _decks.map((deck) => _buildDeckBox(context, deck)).toList();
- var grid = new Grid(deckBoxes, maxChildExtent: style.Size.thumbnailWidth);
+ List<Widget> deckBoxes = _decks.map((deck) => _buildDeckBox(context, deck));
+ List<Widget> presentationBoxes = _store.state.livePresentations
+ .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);
}
}
-// TODO(aghassemi): Is this approach okay? Check with Flutter team.
-// Building RawImage is expensive, so we cache.
-// Expando is a weak map so this does not effect GC.
-Expando<Widget> _weakDeckItemCache = new Expando<Widget>();
Widget _buildDeckBox(BuildContext context, model.Deck deckData) {
- var cachedWidget = _weakDeckItemCache[deckData];
- if (cachedWidget != null) {
- return cachedWidget;
- }
-
- var thumbnail =
- new RawImage(bytes: new Uint8List.fromList(deckData.thumbnail));
-
- var title = new Text(deckData.name, style: style.Text.titleStyle);
- var titleAndActions =
- new Container(child: title, padding: style.Spacing.normalPadding);
-
- var card = new Container(
- child: new Card(child: new Block([thumbnail, titleAndActions])),
- margin: style.Spacing.normalMargin);
-
- var gridItem = new InkWell(child: card, onTap: () {
+ var thumbnail = _buildThumbnail(deckData.thumbnail);
+ // 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 PageRoute(
builder: (context) => new SlideListPage(deckData.key, deckData.name)));
});
- _weakDeckItemCache[deckData] = gridItem;
- return gridItem;
+ return box;
+}
+
+Widget _buildPresentationBox(
+ BuildContext context, model.PresentationAdvertisement presentationData) {
+ var thumbnail = _buildThumbnail(presentationData.deck.thumbnail);
+ 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;
+}
+
+// TODO(aghassemi): Cache will be moved to Flutter and will become a map of encoded bytes to
+// decoded bytes. This is just a quick work-around for now.
+Map<int, RawImage> thumbnailCache = new Map();
+Widget _buildThumbnail(List<int> bytes) {
+ var key = bytes.hashCode;
+ if (thumbnailCache.containsKey(key)) {
+ return thumbnailCache[key];
+ }
+ var thumbnail = new RawImage(bytes: new Uint8List.fromList(bytes));
+
+ thumbnailCache[key] = thumbnail;
+ return thumbnail;
+}
+
+Widget _buildBoxFooter(String title, Widget subtitle) {
+ var titleWidget = new Text(title, style: style.Text.titleStyle);
+ titleWidget = _stopWrapping(titleWidget);
+
+ var titleAndSubtitle = new Block([titleWidget, subtitle]);
+ return new Container(
+ child: titleAndSubtitle, padding: style.Spacing.normalPadding);
+}
+
+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 49cb4c5..cbbb17f 100644
--- a/dart/lib/components/slidelist.dart
+++ b/dart/lib/components/slidelist.dart
@@ -17,6 +17,7 @@
class SlideListPage extends StatelessComponent {
final String deckId;
final String title;
+ Store _store = new Store.singleton();
SlideListPage(this.deckId, this.title);
Widget build(BuildContext context) {
@@ -26,8 +27,20 @@
icon: 'navigation/arrow_back',
onPressed: () => Navigator.of(context).pop()),
center: new Text(title)),
+ floatingActionButton: _buildPresentFab(context),
body: new Material(child: new SlideList(deckId)));
}
+
+ _buildPresentFab(BuildContext context) {
+ return new FloatingActionButton(
+ child: new Icon(icon: 'navigation/arrow_forward'), onPressed: () async {
+ model.PresentationAdvertisement presentation =
+ await _store.startPresentation(deckId);
+ Navigator
+ .of(context)
+ .push(new PageRoute(builder: (context) => new SlideshowPage(deckId)));
+ });
+ }
}
// SlideList is scrollable list view of slides for a deck.
@@ -92,7 +105,7 @@
thumbnail = new Flexible(child: thumbnail);
- var title = new Text('Slide $key', style: style.Text.subTitleStyle);
+ var title = new Text('Slide $key', style: style.Text.subtitleStyle);
var notes = new Text(
'This is the teaser slide. It should be memorable and descriptive');
var titleAndNotes = new Flexible(
diff --git a/dart/lib/discovery/client.dart b/dart/lib/discovery/client.dart
new file mode 100644
index 0000000..e2a766f
--- /dev/null
+++ b/dart/lib/discovery/client.dart
@@ -0,0 +1,185 @@
+// 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 'package:flutter/services.dart' show shell;
+import 'package:v23discovery/discovery.dart' as v23discovery;
+
+import '../models/all.dart' as model;
+import '../utils/asset.dart' as assetutil;
+
+const String v23DiscoveryMojoUrl =
+ 'https://syncslides.mojo.v.io/packages/v23discovery/mojo_services/android/discovery.mojo';
+
+// TODO(aghassemi): We should make this the same between Flutter and Java apps when
+// they can actually talk to each other.
+const String presentationInterfaceName =
+ 'v.io/release/projects/syncslides/dart/presentation';
+
+StreamController<model.PresentationAdvertisement> _onFoundEmitter =
+ new StreamController.broadcast();
+StreamController<String> _onLostEmitter = new StreamController.broadcast();
+
+Stream onFound = _onFoundEmitter.stream;
+Stream onLost = _onLostEmitter.stream;
+
+// TODO(aghassemi): v23discovery could really use a Dart client library.
+// Keep proxy, handle pairs so we can cancel calls later.
+ProxyResponseFuturePair<v23discovery.ScannerProxy,
+ v23discovery.ScannerScanResponseParams> _scanCall;
+
+Map<
+ String,
+ ProxyResponseFuturePair<v23discovery.AdvertiserProxy,
+ v23discovery.AdvertiserAdvertiseResponseParams>> _advertiseCalls =
+ new Map();
+
+Future advertise(model.PresentationAdvertisement presentation) async {
+ if (_advertiseCalls.containsKey(presentation.key)) {
+ // We are already advertising for this presentation.
+ return _advertiseCalls[presentation.key].responseFuture;
+ }
+
+ Map<String, String> serviceAttrs = new Map();
+ serviceAttrs['deckid'] = presentation.deck.key;
+ serviceAttrs['name'] = presentation.deck.name;
+ v23discovery.Service serviceInfo = new v23discovery.Service()
+ ..instanceUuid = UTF8.encode(presentation.key)
+ ..interfaceName = presentationInterfaceName
+ ..instanceName = ''
+ ..attrs = serviceAttrs
+ ..addrs = [presentation.syncgroupName];
+
+ v23discovery.AdvertiserProxy advertiser =
+ new v23discovery.AdvertiserProxy.unbound();
+ shell.connectToService(v23DiscoveryMojoUrl, advertiser);
+ Future advertiseResponseFuture =
+ advertiser.ptr.advertise(serviceInfo, <String>[]);
+ _advertiseCalls[presentation.key] =
+ new ProxyResponseFuturePair(advertiser, advertiseResponseFuture);
+
+ await advertiseResponseFuture;
+}
+
+// Tracks advertisements that are in the middle of being stopped.
+Map<String, Future> _stoppingAdvertisingCalls = new Map<String, Future>();
+Future stopAdvertising(String presentationId) async {
+ if (!_advertiseCalls.containsKey(presentationId)) {
+ // Not advertised, nothing to stop.
+ return new Future.value();
+ }
+
+ if (_stoppingAdvertisingCalls.containsKey(presentationId)) {
+ // Already stopping, return the exiting call future.
+ return _stoppingAdvertisingCalls[presentationId];
+ }
+
+ stop() async {
+ v23discovery.AdvertiserAdvertiseResponseParams advertiserResponse =
+ await _advertiseCalls[presentationId].responseFuture;
+
+ await _advertiseCalls[presentationId]
+ .proxy
+ .ptr
+ .stop(advertiserResponse.handle);
+ await _advertiseCalls[presentationId].proxy.close();
+ }
+
+ Future stoppingCall = stop();
+ _stoppingAdvertisingCalls[presentationId] = stoppingCall;
+
+ stoppingCall.then((_) {
+ _advertiseCalls.remove(presentationId);
+ _stoppingAdvertisingCalls.remove(presentationId);
+ }).catchError((e) {
+ _stoppingAdvertisingCalls.remove(presentationId);
+ throw e;
+ });
+}
+
+Future startScan() async {
+ if (_scanCall != null) {
+ // We are already scanning.
+ return _scanCall.responseFuture;
+ }
+
+ var scanner = new v23discovery.ScannerProxy.unbound();
+ shell.connectToService(v23DiscoveryMojoUrl, scanner);
+ v23discovery.ScanHandlerStub handlerStub =
+ new v23discovery.ScanHandlerStub.unbound();
+ handlerStub.impl = new ScanHandler();
+
+ var query = 'v.InterfaceName = "$presentationInterfaceName"';
+ var scannerResponseFuture = scanner.ptr.scan(query, handlerStub);
+ _scanCall = new ProxyResponseFuturePair(scanner, scannerResponseFuture);
+
+ await scannerResponseFuture;
+}
+
+// Tracks whether we are already in the middle of stopping scan.
+Future _stoppingScanCall;
+Future stopScan() async {
+ if (_scanCall == null) {
+ // No scan call has been made before or scan is already being stopped.
+ return new Future.value();
+ }
+
+ if (_stoppingScanCall != null) {
+ // Already stopping, return the exiting call future.
+ return _stoppingScanCall;
+ }
+
+ stop() async {
+ v23discovery.ScannerScanResponseParams scannerResponse =
+ await _scanCall.responseFuture;
+
+ await _scanCall.proxy.ptr.stop(scannerResponse.handle);
+ await _scanCall.proxy.close();
+ }
+
+ _stoppingScanCall = stop();
+
+ _stoppingScanCall.then((_) {
+ _scanCall = null;
+ _stoppingScanCall = null;
+ }).catchError((e) {
+ _stoppingScanCall = null;
+ throw e;
+ });
+}
+
+class ScanHandler extends v23discovery.ScanHandler {
+ found(v23discovery.Service s) async {
+ String key = UTF8.decode(s.instanceUuid);
+ // Ignore our own advertised services.
+ if (_advertiseCalls.containsKey(key)) {
+ return;
+ }
+
+ // TODO(aghassemi): For now we use the default thumbnail. We need to find a way
+ // to fetch the actual thumbnail from the other side.
+ var thumbnail = await assetutil.getRawBytes(assetutil.defaultThumbnailUrl);
+ model.Deck deck =
+ new model.Deck(s.attrs['deckid'], s.attrs['name'], thumbnail.toList());
+ var syncgroupName = s.addrs[0];
+ model.PresentationAdvertisement presentation =
+ new model.PresentationAdvertisement(key, deck, syncgroupName);
+
+ _onFoundEmitter.add(presentation);
+ }
+
+ lost(List<int> instanceId) {
+ String presentationId = UTF8.decode(instanceId);
+ // Ignore our own advertised services.
+ _onLostEmitter.add(presentationId);
+ }
+}
+
+class ProxyResponseFuturePair<T1, T2> {
+ final T1 proxy;
+ final Future<T2> responseFuture;
+ ProxyResponseFuturePair(this.proxy, this.responseFuture);
+}
diff --git a/dart/lib/loaders/demo_loader.dart b/dart/lib/loaders/demo_loader.dart
index 94d0243..52b4257 100644
--- a/dart/lib/loaders/demo_loader.dart
+++ b/dart/lib/loaders/demo_loader.dart
@@ -3,14 +3,12 @@
// license that can be found in the LICENSE file.
import 'dart:async';
-
import 'dart:math';
-import 'dart:typed_data';
-
-import 'package:flutter/services.dart' as services;
import '../models/all.dart' as model;
import '../stores/store.dart';
+import '../utils/uuid.dart' as uuidutil;
+import '../utils/asset.dart' as assetutil;
import 'loader.dart';
@@ -24,70 +22,60 @@
: _store = new Store.singleton(),
_rand = new Random();
- static const int numDeckSets = 2;
- Stream<model.Deck> _getSampleDecks() async* {
- for (var i = 1; i <= numDeckSets; i++) {
- yield new model.Deck('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'));
+ static final List<String> thumbnails = [
+ 'assets/images/sample_decks/baku/thumb.png',
+ 'assets/images/sample_decks/vanadium/thumb.png',
+ 'assets/images/sample_decks/pitch/thumb.png'
+ ];
+ static final List<String> slides = [
+ 'assets/images/sample_decks/vanadium/1.jpg',
+ 'assets/images/sample_decks/vanadium/2.jpg',
+ 'assets/images/sample_decks/vanadium/3.jpg',
+ 'assets/images/sample_decks/vanadium/4.jpg',
+ 'assets/images/sample_decks/vanadium/5.jpg',
+ 'assets/images/sample_decks/vanadium/6.jpg'
+ ];
+ static final List<String> firstWords = [
+ 'Today\'s',
+ 'Yesterday\'s',
+ 'Ali\'s',
+ 'Adam\'s',
+ 'Misha\'s'
+ ];
+ static final List<String> secondWords = [
+ 'Presentation',
+ 'Slideshow',
+ 'Meeting',
+ 'Pitch',
+ 'Discussion',
+ 'Demo',
+ 'All Hands'
+ ];
+
+ static const int maxNumSlides = 20;
+
+ Future<model.Deck> _getRandomDeck() async {
+ var thumbnail = await assetutil
+ .getRawBytes(thumbnails[_rand.nextInt(thumbnails.length)]);
+ var firstWord = firstWords[_rand.nextInt(firstWords.length)];
+ var secondWord = secondWords[_rand.nextInt(secondWords.length)];
+ return new model.Deck(
+ uuidutil.createUuid(), '$firstWord $secondWord', thumbnail);
+ }
+
+ Stream<model.Slide> _getRandomSlides() async* {
+ 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'));
}
}
- Stream<model.Slide> _getSampleSlides() async* {
- // TODO(aghassemi): We need different slides for different decks.
- // For now use Vanadium slides for all.
- for (var i = 1; i <= 6; i++) {
- yield new model.Slide(
- await _getRawBytes('assets/images/sample_decks/vanadium/$i.jpg'));
- }
- }
-
- Future loadDecks() async {
- // Add some initial decks.
- await for (var deck in _getSampleDecks()) {
- await _addDeck(deck);
- }
-
- // Periodically add or remove random decks.
- new Timer.periodic(new Duration(seconds: 2), (_) async {
- var decks = await _store.getAllDecks();
- var deckKeys = decks.map((d) => d.key);
- var removeDeck = _rand.nextBool();
-
- if (removeDeck && decks.length > 0) {
- 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)) {
- await _addDeck(deck);
- break;
- }
- }
- }
- });
- }
-
- Future _addDeck(model.Deck deck) async {
+ Future addDeck() async {
+ var deck = await _getRandomDeck();
+ List<model.Slide> slides = await _getRandomSlides().toList();
await _store.addDeck(deck);
- List<model.Slide> slides = await _getSampleSlides().toList();
await _store.setSlides(deck.key, slides);
}
-
- Map<String, Uint8List> _assetCache = new Map<String, Uint8List>();
- Future<Uint8List> _getRawBytes(String url) async {
- if (_assetCache.containsKey(url)) {
- return _assetCache[url];
- }
- services.Response response = await services.fetchBody(url);
- var bytes = new Uint8List.fromList(response.body.buffer.asUint8List());
- _assetCache[url] = bytes;
- return bytes;
- }
}
diff --git a/dart/lib/loaders/loader.dart b/dart/lib/loaders/loader.dart
index ec97da1..c206b36 100644
--- a/dart/lib/loaders/loader.dart
+++ b/dart/lib/loaders/loader.dart
@@ -8,14 +8,11 @@
// Loader is responsible for importing existing decks and slides into the store.
abstract class Loader {
- static Loader _singletonLoader;
+ static Loader _singletonLoader = loaderFactory.create();
factory Loader.singleton() {
- if (_singletonLoader == null) {
- _singletonLoader = loaderFactory.create();
- }
return _singletonLoader;
}
- Future loadDecks();
+ Future addDeck();
}
diff --git a/dart/lib/loaders/sdcard_loader.dart b/dart/lib/loaders/sdcard_loader.dart
index 72071c2..a2b0102 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 loadDecks() {
+ Future addDeck() {
throw new UnimplementedError();
}
}
diff --git a/dart/lib/main.dart b/dart/lib/main.dart
index 8cc1ab6..0503e05 100644
--- a/dart/lib/main.dart
+++ b/dart/lib/main.dart
@@ -4,14 +4,12 @@
import 'package:flutter/material.dart';
+import 'styles/common.dart' as style;
import 'components/deckgrid.dart';
-import 'loaders/loader.dart';
void main() {
- // Start loading data.
- new Loader.singleton().loadDecks();
-
runApp(new MaterialApp(
+ theme: style.theme,
title: 'SyncSlides',
routes: {'/': (RouteArguments args) => new DeckGridPage()}));
}
diff --git a/dart/lib/models/all.dart b/dart/lib/models/all.dart
index 61a7d0f..c802c0b 100644
--- a/dart/lib/models/all.dart
+++ b/dart/lib/models/all.dart
@@ -4,3 +4,4 @@
export 'deck.dart';
export 'slide.dart';
+export 'presentation_advertisement.dart';
diff --git a/dart/lib/models/presentation_advertisement.dart b/dart/lib/models/presentation_advertisement.dart
new file mode 100644
index 0000000..5e19c4e
--- /dev/null
+++ b/dart/lib/models/presentation_advertisement.dart
@@ -0,0 +1,20 @@
+// 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 '../models/all.dart' as model;
+
+// Represents an advertised presentation of a deck.
+class PresentationAdvertisement {
+ // TODO(aghassemi): Fix inconsistencies between key and id everywhere.
+ String _key;
+ String get key => _key;
+
+ model.Deck _deck;
+ model.Deck get deck => _deck;
+
+ String _syncgroupName;
+ String get syncgroupName => _syncgroupName;
+
+ PresentationAdvertisement(this._key, this._deck, this._syncgroupName) {}
+}
diff --git a/dart/lib/stores/memory_store.dart b/dart/lib/stores/memory_store.dart
index aa80f74..bfee15f 100644
--- a/dart/lib/stores/memory_store.dart
+++ b/dart/lib/stores/memory_store.dart
@@ -7,6 +7,7 @@
import '../models/all.dart' as model;
import 'keyutil.dart' as keyutil;
+import 'state.dart';
import 'store.dart';
// A memory-based implementation of Store.
@@ -16,16 +17,26 @@
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();
+ _currSlideNumChangeEmitterMap = new Map(),
+ _state = new State(),
+ _stateChangeEmitter = new StreamController.broadcast();
//////////////////////////////////////
- /// Decks
+ // State
+
+ State get state => _state;
+ Stream get onStateChange => _stateChangeEmitter.stream;
+
+ //////////////////////////////////////
+ // Decks
Future<List<model.Deck>> getAllDecks() async {
var decks = [];
@@ -36,6 +47,10 @@
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;
@@ -55,7 +70,7 @@
Stream<List<model.Deck>> get onDecksChange => _onDecksChangeEmitter.stream;
//////////////////////////////////////
- /// Slides
+ // Slides
Future<List<model.Slide>> getAllSlides(String deckKey) async {
var slides = [];
@@ -99,4 +114,22 @@
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
new file mode 100644
index 0000000..3ecf6bd
--- /dev/null
+++ b/dart/lib/stores/state.dart
@@ -0,0 +1,19 @@
+// 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 '../models/all.dart' as model;
+
+// 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.
+
+ // TODO(aghassemi): State needs to be deeply immutable.
+ // Maybe https://github.com/google/built_value.dart can help?
+ List<model.PresentationAdvertisement> livePresentations;
+
+ State() : livePresentations = new List();
+}
diff --git a/dart/lib/stores/store.dart b/dart/lib/stores/store.dart
index 06eadfd..6c817c8 100644
--- a/dart/lib/stores/store.dart
+++ b/dart/lib/stores/store.dart
@@ -6,36 +6,36 @@
import '../models/all.dart' as model;
+import 'state.dart';
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;
+ static Store _singletonStore = storeFactory.create();
factory Store.singleton() {
- if (_singletonStore == null) {
- _singletonStore = storeFactory.create();
- }
return _singletonStore;
}
//////////////////////////////////////
- /// Decks
+ // State
+
+ State 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);
- // Removed a deck given its key.
+ // Removes a deck given its key.
Future removeDeck(String key);
// Event that fires when deck are added or removed.
@@ -43,7 +43,7 @@
Stream<List<model.Deck>> get onDecksChange;
//////////////////////////////////////
- /// Slides
+ // Slides
// Returns the list of all slides for a deck.
Future<List<model.Slide>> getAllSlides(String deckKey);
@@ -59,4 +59,13 @@
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/syncbase_store.dart b/dart/lib/stores/syncbase_store.dart
index 2bfd564..2a98a33 100644
--- a/dart/lib/stores/syncbase_store.dart
+++ b/dart/lib/stores/syncbase_store.dart
@@ -5,11 +5,14 @@
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';
@@ -18,18 +21,31 @@
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);
- _startSync(db);
+ _startScanningForPresentations();
});
}
//////////////////////////////////////
- /// Decks
+ // 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:
@@ -42,9 +58,17 @@
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()));
@@ -59,16 +83,53 @@
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 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
+ // Slides
Future<List<model.Slide>> getAllSlides(String deckKey) async {
// Key schema is:
@@ -131,45 +192,54 @@
return _currSlideNumChangeEmitterMap[deckId];
}
- 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;
- }
+ //////////////////////////////////////
+ // 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.');
}
- return tb;
+
+ 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 _startDecksWatch(sb.SyncbaseNoSqlDatabase sbDb) async {
- var resumeMarker = await sbDb.getResumeMarker();
- var stream = sbDb.watch(decksTableName, '', resumeMarker);
+ Future stopPresentation(String presentationId) async {
+ await discovery.stopAdvertising(presentationId);
+ _advertisedPresentations.removeWhere((p) => p.key == presentationId);
+ }
- 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);
- }
- }
- }
+ 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();
});
- }
- Future _startSync(sb.SyncbaseNoSqlDatabase sbDb) async {}
+ discovery.onLost.listen((String pId) {
+ state.livePresentations.removeWhere((p) => p.key == pId);
+ _triggerStateChange();
+ });
+
+ discovery.startScan();
+ }
}
diff --git a/dart/lib/styles/common.dart b/dart/lib/styles/common.dart
index 105af9a..d275af0 100644
--- a/dart/lib/styles/common.dart
+++ b/dart/lib/styles/common.dart
@@ -2,13 +2,15 @@
// 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';
class Text {
- static final Color secondaryTextColor = new Color.fromARGB(70, 0, 0, 0);
+ static final Color secondaryTextColor = Colors.grey[500];
static final TextStyle titleStyle = new TextStyle(fontSize: 18.0);
- static final TextStyle subTitleStyle =
+ static final TextStyle subtitleStyle =
new TextStyle(fontSize: 12.0, color: secondaryTextColor);
+ static final TextStyle liveNow =
+ new TextStyle(fontSize: 12.0, color: theme.accentColor);
}
class Size {
@@ -19,8 +21,18 @@
}
class Spacing {
+ static final EdgeDims extraSmallPadding = new EdgeDims.all(2.0);
+ static final EdgeDims smallPadding = new EdgeDims.all(5.0);
static final EdgeDims normalPadding = new EdgeDims.all(10.0);
static final EdgeDims normalMargin = new EdgeDims.all(2.0);
static final EdgeDims listItemMargin = new EdgeDims.TRBL(3.0, 6.0, 0.0, 6.0);
static final EdgeDims thumbnailNavMargin = new EdgeDims.all(3.0);
}
+
+class Box {
+ static final BoxDecoration liveNow = new BoxDecoration(
+ border: new Border.all(color: theme.accentColor), borderRadius: 2.0);
+}
+
+ThemeData theme = new ThemeData(
+ primarySwatch: Colors.blueGrey, accentColor: Colors.orangeAccent[700]);
diff --git a/dart/lib/utils/asset.dart b/dart/lib/utils/asset.dart
new file mode 100644
index 0000000..bb001a3
--- /dev/null
+++ b/dart/lib/utils/asset.dart
@@ -0,0 +1,21 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:typed_data';
+
+import 'package:flutter/services.dart' as services;
+
+Map<String, Uint8List> _assetCache = new Map<String, Uint8List>();
+Future<Uint8List> getRawBytes(String url) async {
+ if (_assetCache.containsKey(url)) {
+ return _assetCache[url];
+ }
+ services.Response response = await services.fetchBody(url);
+ var bytes = new Uint8List.fromList(response.body.buffer.asUint8List());
+ _assetCache[url] = bytes;
+ return bytes;
+}
+
+String defaultThumbnailUrl = 'assets/images/defaults/thumbnail.png';
diff --git a/dart/lib/utils/uuid.dart b/dart/lib/utils/uuid.dart
new file mode 100644
index 0000000..90e6d12
--- /dev/null
+++ b/dart/lib/utils/uuid.dart
@@ -0,0 +1,12 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+import 'package:uuid/uuid.dart';
+
+Uuid _uuid = new Uuid();
+
+// Creates a universally unique identifier.
+String createUuid() {
+ return _uuid.v4();
+}
diff --git a/dart/pubspec.lock b/dart/pubspec.lock
index 7eaa90a..a8af949 100644
--- a/dart/pubspec.lock
+++ b/dart/pubspec.lock
@@ -72,11 +72,11 @@
flutter:
description: flutter
source: hosted
- version: "0.0.17"
+ version: "0.0.18"
flx:
description: flx
source: hosted
- version: "0.0.9"
+ version: "0.0.10"
glob:
description: glob
source: hosted
@@ -116,23 +116,23 @@
mojo:
description: mojo
source: hosted
- version: "0.3.0"
+ version: "0.4.2"
mojo_apptest:
description: mojo_apptest
source: hosted
- version: "0.2.3"
+ version: "0.2.7"
mojo_sdk:
description: mojo_sdk
source: hosted
- version: "0.1.0"
+ version: "0.2.1"
mojo_services:
description: mojo_services
source: hosted
- version: "0.4.0"
+ version: "0.4.4"
mojom:
description: mojom
source: hosted
- version: "0.2.3"
+ version: "0.2.7"
mustache4dart:
description: mustache4dart
source: hosted
@@ -196,11 +196,11 @@
sky_engine:
description: sky_engine
source: hosted
- version: "0.0.48"
+ version: "0.0.49"
sky_services:
description: sky_services
source: hosted
- version: "0.0.48"
+ version: "0.0.50"
sky_tools:
description: sky_tools
source: hosted
@@ -228,7 +228,7 @@
syncbase:
description: syncbase
source: hosted
- version: "0.0.11"
+ version: "0.0.14"
test:
description: test
source: hosted
@@ -245,6 +245,14 @@
description: utf
source: hosted
version: "0.9.0+2"
+ uuid:
+ description: uuid
+ source: hosted
+ version: "0.5.0"
+ v23discovery:
+ description: v23discovery
+ source: hosted
+ version: "0.0.2"
vector_math:
description: vector_math
source: hosted
diff --git a/dart/pubspec.yaml b/dart/pubspec.yaml
index 0d49473..b44d99c 100644
--- a/dart/pubspec.yaml
+++ b/dart/pubspec.yaml
@@ -3,5 +3,7 @@
dependencies:
flutter: ">=0.0.16 <0.1.0"
syncbase: ">=0.0.9 <0.1.0"
+ v23discovery: ">=0.0.1 < 0.1.0"
+ uuid: ">=0.5.0 <0.6.0"
dev_dependencies:
sky_tools: ">=0.0.27 <0.1.0"