Syncslides: Slideview UI
Screenshots: https://goo.gl/photos/na5ew2Mk86Nc9rnw5
-Landscape and Portrait support.
-Actionbar and (readonly) notes
-Immersive slide view.
-Simplified the state and components a bit.
Change-Id: Id97d11f6759856010a591242eba7b131e62f2b4e
diff --git a/dart/flutter.yaml b/dart/flutter.yaml
index 47b433e..00ecac9 100644
--- a/dart/flutter.yaml
+++ b/dart/flutter.yaml
@@ -4,6 +4,7 @@
- name: navigation/arrow_forward
- name: content/add
- name: notification/sync
+ - name: maps/layers
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 71771e3..f3dbe30 100644
--- a/dart/lib/components/deckgrid.dart
+++ b/dart/lib/components/deckgrid.dart
@@ -23,9 +23,12 @@
}
Widget build(BuildContext context, AppState appState, AppActions appActions) {
+ // Local decks.
List<model.Deck> decks = appState.decks.values
- .where((DeckState d) => d.deck != null)
- .map((d) => d.deck);
+ .where((DeckState d) => d.deck != null && d.presentation == null)
+ .map((DeckState d) => d.deck);
+
+ // Advertised decks.
List<model.PresentationAdvertisement> presentations =
appState.presentationAdvertisements.values;
@@ -98,11 +101,11 @@
// 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)));
+ builder: (context) =>
+ new SlideListPage(presentationData.deck.key)));
Navigator.of(context).push(new MaterialPageRoute(
- builder: (context) => new SlideshowPage(presentationData.deck.key,
- presentationId: presentationData.key)));
+ builder: (context) =>
+ new SlideshowPage(presentationData.deck.key)));
} catch (e) {
toast.error(_scaffoldKey,
'Failed to start presentation ${presentationData.deck.name}.', e);
diff --git a/dart/lib/components/slidelist.dart b/dart/lib/components/slidelist.dart
index a680b35..9de7fea 100644
--- a/dart/lib/components/slidelist.dart
+++ b/dart/lib/components/slidelist.dart
@@ -16,10 +16,8 @@
class SlideListPage extends SyncSlidesPage {
final String _deckId;
- final String _presentationId;
- SlideListPage(this._deckId, {String presentationId})
- : _presentationId = presentationId;
+ SlideListPage(this._deckId);
Widget build(BuildContext context, AppState appState, AppActions appActions) {
if (!appState.decks.containsKey(_deckId)) {
@@ -42,16 +40,9 @@
_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) {
+ var deckState = appState.decks[_deckId];
+ if (deckState.presentation != null) {
+ // Can't present when already in a presentation.
return null;
}
@@ -61,12 +52,11 @@
duration: toast.Durations.permanent);
try {
- var presentation = await appActions.startPresentation(_deckId);
+ await appActions.startPresentation(_deckId);
toast.info(_scaffoldKey, 'Presentation started.');
Navigator.of(context).push(new MaterialPageRoute(
- builder: (context) =>
- new SlideshowPage(_deckId, presentationId: presentation.key)));
+ builder: (context) => new SlideshowPage(_deckId)));
} catch (e) {
toast.error(_scaffoldKey, 'Failed to start presentation.', e);
}
@@ -76,12 +66,9 @@
class SlideList extends StatelessComponent {
String _deckId;
- String _presentationId;
List<model.Slide> _slides = new List<model.Slide>();
AppActions _appActions;
- SlideList(this._deckId, this._slides, this._appActions,
- {String presentationId})
- : _presentationId = presentationId;
+ SlideList(this._deckId, this._slides, this._appActions);
Widget build(BuildContext context) {
NavigatorState navigator = Navigator.of(context);
@@ -90,12 +77,10 @@
items: _slides,
itemBuilder: (context, value, index) =>
_buildSlide(context, _deckId, index, value, onTap: () {
- _appActions.setCurrSlideNum(_deckId, index,
- presentationId: _presentationId);
+ _appActions.setCurrSlideNum(_deckId, index);
navigator.push(new MaterialPageRoute(
- builder: (context) => new SlideshowPage(_deckId,
- presentationId: _presentationId)));
+ builder: (context) => new SlideshowPage(_deckId)));
}));
}
}
diff --git a/dart/lib/components/slideshow.dart b/dart/lib/components/slideshow.dart
index cf301f9..8fb94a6 100644
--- a/dart/lib/components/slideshow.dart
+++ b/dart/lib/components/slideshow.dart
@@ -6,20 +6,18 @@
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 'slideshow_immersive.dart';
import 'syncslides_page.dart';
-final Logger log = new Logger('store/syncbase_store');
+final Logger log = new Logger('components/slideshow');
class SlideshowPage extends SyncSlidesPage {
final String _deckId;
- final String _presentationId;
- SlideshowPage(this._deckId, {String presentationId})
- : _presentationId = presentationId;
+ SlideshowPage(this._deckId);
Widget build(BuildContext context, AppState appState, AppActions appActions) {
if (!appState.decks.containsKey(_deckId)) {
@@ -27,67 +25,36 @@
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())),
- 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);
- });
+ child: new SlideShow(appActions, appState.decks[_deckId])));
}
}
class SlideShow extends StatelessComponent {
AppActions _appActions;
DeckState _deckState;
- PresentationState _presentationState;
+ NavigatorState _navigator;
+ int _currSlideNum;
- SlideShow(this._appActions, this._deckState, this._presentationState);
+ SlideShow(this._appActions, this._deckState);
Widget build(BuildContext context) {
+ _navigator = Navigator.of(context);
+
if (_deckState.slides.length == 0) {
// TODO(aghassemi): Proper error page with navigation back to main view.
return new Text('No slide to show.');
}
- int currSlideNum;
-
- bool isFollowingPresenter =
- _presentationState != null && !_presentationState.isNavigationOutOfSync;
-
- if (isFollowingPresenter) {
- currSlideNum = _presentationState.currSlideNum;
+ if (_deckState.presentation != null &&
+ _deckState.presentation.isFollowingPresentation) {
+ _currSlideNum = _deckState.presentation.currSlideNum;
} else {
- currSlideNum = _deckState.currSlideNum;
+ _currSlideNum = _deckState.currSlideNum;
}
- if (currSlideNum >= _deckState.slides.length) {
+ 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?
@@ -95,53 +62,213 @@
// 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}');
+ '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(_deckState.deck.key, slideData),
- fit: ImageFit.contain);
- var navWidgets = [
- _buildSlideNav(currSlideNum - 1),
- _buildSlideNav(currSlideNum + 1)
- ];
-
- return new Block(
- [image, new Text(currSlideNum.toString()), new Row(navWidgets)]);
+ if (MediaQuery.of(context).orientation == Orientation.landscape) {
+ return _buildLandscapeLayout(context);
+ } else {
+ return _buildPortraitLayout(context);
+ }
}
- Widget _buildSlideNav(int slideNum) {
- var card;
+ Widget _buildPortraitLayout(BuildContext context) {
+ // Portrait mode is a column layout divided as 5 parts image, 1 part actionbar
+ // 3 parts notes and 3 parts next/previous navigation thumbnails.
+ var image = new Flexible(child: _buildImage(), flex: 5);
+ var actions = new Flexible(child: _buildActions(), flex: 1);
+ var notes = new Flexible(child: _buildNotes(), flex: 3);
+ var nav = new Flexible(child: _buildPortraitNav(), flex: 3);
+ var layout = new Column([image, actions, notes, nav],
+ alignItems: FlexAlignItems.stretch);
+
+ return layout;
+ }
+
+ Widget _buildLandscapeLayout(BuildContext context) {
+ // Landscape mode is a two column layout.
+ // First column is divided as 5 parts notes, 8 parts parts next/previous navigation thumbnails.
+ // Second column is divided as 11 parts image, 2 parts actionbar.
+ var notes = new Flexible(child: _buildNotes(), flex: 5);
+ var nav = new Flexible(child: _buildLandscapeNav(), flex: 8);
+
+ var image = new Flexible(child: _buildImage(), flex: 11);
+ var actions = new Flexible(child: _buildActions(), flex: 2);
+
+ var notesAndNavColumn = new Flexible(
+ child: new Column([notes, nav], alignItems: FlexAlignItems.stretch),
+ flex: 4);
+ var imageAndActionsColumn = new Flexible(
+ child: new Column([image, actions], alignItems: FlexAlignItems.stretch),
+ flex: 16);
+
+ var layout = new Row([notesAndNavColumn, imageAndActionsColumn],
+ alignItems: FlexAlignItems.stretch);
+
+ return layout;
+ }
+
+ Widget _buildPortraitNav() {
+ return new Row([
+ _buildThumbnailNav(_currSlideNum - 1),
+ _buildThumbnailNav(_currSlideNum + 1)
+ ]);
+ }
+
+ Widget _buildLandscapeNav() {
+ return new Column([
+ _buildThumbnailNav(_currSlideNum - 1),
+ _buildThumbnailNav(_currSlideNum + 1)
+ ]);
+ }
+
+ Widget _buildImage() {
+ var provider = imageProvider.getSlideImage(
+ _deckState.deck.key, _deckState.slides[_currSlideNum]);
+
+ var image = new AsyncImage(provider: provider);
+
+ // If not driving the presentation, tapping the image navigates to the immersive mode.
+ if (_deckState.presentation == null || !_deckState.presentation.isDriving) {
+ image = new InkWell(child: image, onTap: () {
+ _navigator.push(new MaterialPageRoute(
+ builder: (context) =>
+ new SlideshowImmersivePage(_deckState.deck.key)));
+ });
+ }
+
+ return new Row([image],
+ justifyContent: FlexJustifyContent.center,
+ alignItems: FlexAlignItems.stretch);
+ }
+
+ Widget _buildNotes() {
+ // TODO(aghassemi): Notes data.
+ var notes =
+ new Text('Notes (only you see these)', style: style.Text.subtitleStyle);
+ var container = new Container(
+ child: notes,
+ padding: style.Spacing.normalPadding,
+ decoration: new BoxDecoration(
+ border: new Border(
+ bottom: new BorderSide(color: style.theme.dividerColor))));
+ return container;
+ }
+
+ Widget _buildThumbnailNav(int slideNum) {
+ var container;
if (slideNum >= 0 && slideNum < _deckState.slides.length) {
- var onTap = () => _appActions.setCurrSlideNum(
- _deckState.deck.key, slideNum,
- presentationId: _presentationState?.key);
+ var thumbnail = new AsyncImage(
+ provider: imageProvider.getSlideImage(
+ _deckState.deck.key, _deckState.slides[slideNum]),
+ fit: ImageFit.scaleDown);
- card = _buildThumbnailNav(
- _deckState.deck.key, slideNum, _deckState.slides[slideNum],
- onTap: onTap);
+ container = new Row([thumbnail]);
+ container = new InkWell(child: container, onTap: () {
+ _appActions.setCurrSlideNum(_deckState.deck.key, slideNum);
+ });
} else {
- card = new Container(
- width: style.Size.thumbnailNavWidth,
- height: style.Size.thumbnailNavHeight);
+ // Empty grey placeholder.
+ container = new Container(
+ decoration: new BoxDecoration(
+ backgroundColor: style.theme.primarySwatch[100]));
}
- // TODO(dynin): overlay 'Previous' / 'Next' text
- return new Container(child: card, margin: style.Spacing.thumbnailNavMargin);
+ return new Flexible(child: container, flex: 1);
}
-}
-Widget _buildThumbnailNav(String deckId, int slideIndex, model.Slide slideData,
- {Function onTap}) {
- var thumbnail = new AsyncImage(
- provider: imageProvider.getSlideImage(deckId, slideData),
- height: style.Size.thumbnailNavHeight,
- fit: ImageFit.cover);
+ Widget _buildActions() {
+ // It collects a list of action widgets for the action bar and fabs.
+ // Left contains items that are in-line on the left side of the UI.
+ // Right contains the FABs that hover over the right side of the UI.
+ List<Widget> left = [];
+ List<Widget> right = [];
- return new InkWell(child: thumbnail, onTap: onTap);
+ _buildActions_prev(left, right);
+ _buildActions_slidelist(left, right);
+ _buildActions_next(left, right);
+ _buildActions_followPresentation(left, right);
+
+ return new ToolBar(
+ left: new Row(_buildActions_addMargin(left)), right: right);
+ }
+
+ void _buildActions_prev(List<Widget> left, List<Widget> right) {
+ if (_currSlideNum == 0) {
+ return;
+ }
+ var prev =
+ new InkWell(child: new Icon(icon: 'navigation/arrow_back'), onTap: () {
+ _appActions.setCurrSlideNum(_deckState.deck.key, _currSlideNum - 1);
+ });
+ left.add(prev);
+ }
+
+ void _buildActions_slidelist(List<Widget> left, List<Widget> right) {
+ var slideList =
+ new InkWell(child: new Icon(icon: 'maps/layers'), onTap: () {
+ _navigator.pop();
+ });
+ left.add(slideList);
+ }
+
+ final Matrix4 moveUpFabTransform =
+ new Matrix4.identity().translate(0.0, -27.5);
+
+ void _buildActions_next(List<Widget> left, List<Widget> right) {
+ if (_currSlideNum >= (_deckState.slides.length - 1)) {
+ return;
+ }
+
+ var nextOnTap = () {
+ _appActions.setCurrSlideNum(_deckState.deck.key, _currSlideNum + 1);
+ };
+
+ // If driving the presentation, show a bigger FAB next button on the right side,
+ // otherwise a regular next button on the left side.
+ if (_deckState.presentation != null && _deckState.presentation.isDriving) {
+ var next = new FloatingActionButton(
+ child: new Icon(icon: 'navigation/arrow_forward'),
+ onPressed: nextOnTap);
+
+ var container =
+ new Container(child: next, margin: style.Spacing.fabMargin);
+ next = new Transform(transform: moveUpFabTransform, child: container);
+
+ right.add(next);
+ } else {
+ var next = new InkWell(
+ child: new Icon(icon: 'navigation/arrow_forward'), onTap: nextOnTap);
+ left.add(next);
+ }
+ }
+
+ void _buildActions_followPresentation(List<Widget> left, List<Widget> right) {
+ if (_deckState.presentation == null ||
+ _deckState.presentation.isFollowingPresentation) {
+ return;
+ }
+
+ var syncNav = new FloatingActionButton(
+ child: new Icon(icon: 'notification/sync'), onPressed: () async {
+ _appActions.followPresentation(_deckState.deck.key);
+ });
+
+ syncNav =
+ new Container(child: syncNav, margin: style.Spacing.actionsMargin);
+ syncNav = new Transform(transform: moveUpFabTransform, child: syncNav);
+
+ right.add(syncNav);
+ }
+
+ _buildActions_addMargin(List<Widget> actions) {
+ return actions
+ .map(
+ (w) => new Container(child: w, margin: style.Spacing.actionsMargin))
+ .toList();
+ }
}
diff --git a/dart/lib/components/slideshow_immersive.dart b/dart/lib/components/slideshow_immersive.dart
new file mode 100644
index 0000000..b6528c0
--- /dev/null
+++ b/dart/lib/components/slideshow_immersive.dart
@@ -0,0 +1,36 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
+
+import '../stores/store.dart';
+import '../utils/image_provider.dart' as imageProvider;
+import 'slideshow.dart';
+
+class SlideshowImmersivePage extends SlideshowPage {
+ String _deckId;
+
+ SlideshowImmersivePage(String deckId) : super(deckId) {
+ _deckId = deckId;
+ }
+
+ @override
+ Widget build(BuildContext context, AppState appState, AppActions appActions) {
+ var deckState = appState.decks[_deckId];
+
+ int currSlideNum;
+
+ if (deckState.presentation != null &&
+ deckState.presentation.isFollowingPresentation) {
+ currSlideNum = deckState.presentation.currSlideNum;
+ } else {
+ currSlideNum = deckState.currSlideNum;
+ }
+ var provider = imageProvider.getSlideImage(
+ deckState.deck.key, deckState.slides[currSlideNum]);
+
+ return new AsyncImage(provider: provider);
+ }
+}
diff --git a/dart/lib/discovery/client.dart b/dart/lib/discovery/client.dart
index f9eab6e..72e0668 100644
--- a/dart/lib/discovery/client.dart
+++ b/dart/lib/discovery/client.dart
@@ -59,8 +59,7 @@
v23discovery.AdvertiserProxy advertiser =
new v23discovery.AdvertiserProxy.unbound();
shell.connectToService(v23DiscoveryMojoUrl, advertiser);
- Future advertiseResponseFuture =
- advertiser.ptr.advertise(serviceInfo, <String>[]);
+ Future advertiseResponseFuture = advertiser.ptr.advertise(serviceInfo, null);
_advertiseCalls[presentation.key] =
new ProxyResponseFuturePair(advertiser, advertiseResponseFuture);
diff --git a/dart/lib/stores/actions.dart b/dart/lib/stores/actions.dart
index 093bab8..2c6c085 100644
--- a/dart/lib/stores/actions.dart
+++ b/dart/lib/stores/actions.dart
@@ -25,7 +25,7 @@
Future setSlides(String deckKey, List<model.Slide> slides);
// Sets the current slide number for a deck.
- Future setCurrSlideNum(String deckId, int slideNum, {String presentationId});
+ Future setCurrSlideNum(String deckId, int slideNum);
//////////////////////////////////////
// Presentation
@@ -42,7 +42,7 @@
// Stops all presentations.
Future stopAllPresentations();
- // If viewer has started navigating on their own, this will sync the navigation
+ // If viewer has started navigating on their own, this will align the navigation
// back up with the presentation.
- Future syncUpNavigationWithPresentation(String deckId, String presentationId);
+ Future followPresentation(String deckId);
}
diff --git a/dart/lib/stores/state.dart b/dart/lib/stores/state.dart
index a3afb59..c9e099c 100644
--- a/dart/lib/stores/state.dart
+++ b/dart/lib/stores/state.dart
@@ -9,7 +9,6 @@
// 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,
@@ -19,6 +18,7 @@
abstract class DeckState {
model.Deck get deck;
UnmodifiableListView<model.Slide> get slides;
+ PresentationState get presentation;
int get currSlideNum;
}
@@ -26,5 +26,5 @@
String get key;
int get currSlideNum;
bool get isDriving;
- bool get isNavigationOutOfSync;
+ bool get isFollowingPresentation;
}
diff --git a/dart/lib/stores/syncbase/actions.dart b/dart/lib/stores/syncbase/actions.dart
index 2fe3428..978a924 100644
--- a/dart/lib/stores/syncbase/actions.dart
+++ b/dart/lib/stores/syncbase/actions.dart
@@ -34,8 +34,7 @@
});
}
- Future setCurrSlideNum(String deckId, int slideNum,
- {String presentationId}) async {
+ Future setCurrSlideNum(String deckId, int slideNum) async {
var deckState = _state._getOrCreateDeckState(deckId);
if (slideNum < 0 || slideNum >= deckState.slides.length) {
throw new ArgumentError.value(slideNum,
@@ -48,25 +47,18 @@
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];
-
+ if (deckState.presentation != null) {
// Is the current user driving the presentation?
- if (presentationState.isDriving) {
+ if (deckState.presentation.isDriving) {
// Update the common slide number for the presentation.
sb.SyncbaseTable tb = await _getPresentationsTable();
await tb.put(
- keyutil.getPresentationCurrSlideNumKey(deckId, presentationId),
+ keyutil.getPresentationCurrSlideNumKey(
+ deckId, deckState.presentation.key),
[slideNum]);
} else {
// User is not driving the presentation so they are navigating on their own.
- presentationState._isNavigationOutOfSync = true;
+ deckState.presentation._isFollowingPresentation = false;
}
}
_emitChange();
@@ -119,40 +111,56 @@
Future joinPresentation(model.PresentationAdvertisement presentation) async {
bool isMyOwnPresentation =
_state._advertisedPresentations.any((p) => p.key == presentation.key);
+ String deckId = presentation.deck.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));
- }
-
+ // Set the presentation state for the deck.
+ _DeckState deckState = _state._getOrCreateDeckState(deckId);
_PresentationState presentationState =
- _state._getOrCreatePresentationState(presentation.key);
+ deckState._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}');
+ if (!isMyOwnPresentation) {
+ // Wait until at least the slide for current page number is synced.
+ join() async {
+ await sb.joinSyncgroup(presentation.syncgroupName);
+ Completer completer = new Completer();
+ 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));
+ }
+
+ try {
+ // For for join. If it fails, remove the presentation state from the deck.
+ await join();
+ } catch (e) {
+ deckState._presentation = null;
+ throw e;
+ }
+
+ 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);
+ _state._decks.values.forEach((_DeckState deck) {
+ if (deck.presentation != null &&
+ deck.presentation.key == presentationId) {
+ deck._presentation = null;
+ }
+ });
log.info('Presentation $presentationId stopped');
}
@@ -164,19 +172,17 @@
}));
}
- Future syncUpNavigationWithPresentation(
- String deckId, String presentationId) async {
- if (!_state.presentations.containsKey(presentationId)) {
- throw new ArgumentError.value(
- presentationId, "Presentation does not exist.");
+ Future followPresentation(String deckId) async {
+ var deckState = _state._getOrCreateDeckState(deckId);
+
+ if (deckState.presentation == null) {
+ throw new ArgumentError.value(deckId, 'Deck is not being presented.');
}
- _PresentationState presentationState = _state.presentations[presentationId];
-
// Set the current slide number to the presentation's current slide number.
- await setCurrSlideNum(deckId, presentationState.currSlideNum);
+ await setCurrSlideNum(deckId, deckState.presentation.currSlideNum);
- presentationState._isNavigationOutOfSync = false;
+ deckState.presentation._isFollowingPresentation = true;
}
}
diff --git a/dart/lib/stores/syncbase/state.dart b/dart/lib/stores/syncbase/state.dart
index f2206b6..060b284 100644
--- a/dart/lib/stores/syncbase/state.dart
+++ b/dart/lib/stores/syncbase/state.dart
@@ -14,7 +14,6 @@
presentationAdvertisements =
new UnmodifiableMapView(_presentationsAdvertisements);
decks = new UnmodifiableMapView(_decks);
- presentations = new UnmodifiableMapView(_presentations);
advertisedPresentations =
new UnmodifiableListView(_advertisedPresentations);
}
@@ -22,7 +21,6 @@
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) {
@@ -30,12 +28,6 @@
return new _DeckState();
});
}
-
- _PresentationState _getOrCreatePresentationState(String presentationId) {
- return _presentations.putIfAbsent(presentationId, () {
- return new _PresentationState(presentationId);
- });
- }
}
class _DeckState implements DeckState {
@@ -45,12 +37,22 @@
List<model.Slide> _slides = new List();
List<model.Slide> slides;
+ PresentationState _presentation = null;
+ PresentationState get presentation => _presentation;
+
int _currSlideNum = 0;
int get currSlideNum => _currSlideNum;
_DeckState() {
slides = new UnmodifiableListView(_slides);
}
+
+ _PresentationState _getOrCreatePresentationState(String presentationId) {
+ if (_presentation == null) {
+ _presentation = new _PresentationState(presentationId);
+ }
+ return _presentation;
+ }
}
class _PresentationState implements PresentationState {
@@ -62,8 +64,8 @@
bool _isDriving = false;
bool get isDriving => _isDriving;
- bool _isNavigationOutOfSync = false;
- bool get isNavigationOutOfSync => _isNavigationOutOfSync;
+ bool _isFollowingPresentation = true;
+ bool get isFollowingPresentation => _isFollowingPresentation;
_PresentationState(this.key);
}
diff --git a/dart/lib/stores/syncbase/store.dart b/dart/lib/stores/syncbase/store.dart
index 4095383..c4105f9 100644
--- a/dart/lib/stores/syncbase/store.dart
+++ b/dart/lib/stores/syncbase/store.dart
@@ -65,8 +65,14 @@
_triggerStateChange();
});
- discovery.onLost.listen((String pId) {
- _state._presentationsAdvertisements.remove(pId);
+ discovery.onLost.listen((String presentationId) {
+ _state._presentationsAdvertisements.remove(presentationId);
+ _state._decks.values.forEach((_DeckState deck) {
+ if (deck.presentation != null &&
+ deck.presentation.key == presentationId) {
+ deck._presentation = null;
+ }
+ });
_triggerStateChange();
});
@@ -149,10 +155,13 @@
_onPresentationSlideNumChange(
int changeType, String rowKey, List<int> value) {
- var presentationId =
- keyutil.presentationCurrSlideNumKeyToPresentationId(rowKey);
- var presentationState =
- _state._getOrCreatePresentationState(presentationId);
+ String deckId = keyutil.presentationCurrSlideNumKeyToDeckId(rowKey);
+
+ _DeckState deckState = _state._getOrCreateDeckState(deckId);
+ _PresentationState presentationState = deckState.presentation;
+ if (presentationState == null) {
+ return;
+ }
if (changeType == sb.WatchChangeTypes.put) {
int currSlideNum = value[0];
presentationState._currSlideNum = currSlideNum;
diff --git a/dart/lib/stores/utils/key.dart b/dart/lib/stores/utils/key.dart
index 033368d..a8943ad 100644
--- a/dart/lib/stores/utils/key.dart
+++ b/dart/lib/stores/utils/key.dart
@@ -63,15 +63,15 @@
// 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)');
+ 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) {
+// Gets the deck id given a current slide number key.
+String presentationCurrSlideNumKeyToDeckId(String currSlideNumKey) {
if ((!isPresentationCurrSlideNumKey(currSlideNumKey))) {
throw new ArgumentError(
"$currSlideNumKey is not a valid presentation current slide number key.");
diff --git a/dart/lib/styles/common.dart b/dart/lib/styles/common.dart
index ccbef24..17962e8 100644
--- a/dart/lib/styles/common.dart
+++ b/dart/lib/styles/common.dart
@@ -28,7 +28,8 @@
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);
+ static final EdgeDims actionsMargin = new EdgeDims.only(right: 20.0);
+ static final EdgeDims fabMargin = new EdgeDims.only(right: 7.0);
}
class Box {
diff --git a/dart/lib/utils/image_provider.dart b/dart/lib/utils/image_provider.dart
index a716b07..d4029c1 100644
--- a/dart/lib/utils/image_provider.dart
+++ b/dart/lib/utils/image_provider.dart
@@ -11,11 +11,20 @@
import '../models/all.dart' as model;
ImageProvider getDeckThumbnailImage(model.Deck deck) {
+ if (deck == null) {
+ throw new ArgumentError.notNull('deck');
+ }
return new _RawImageProvider('thumbnail_${deck.key}', deck.thumbnail);
}
ImageProvider getSlideImage(String deckId, model.Slide slide) {
- return new _RawImageProvider('slide_${deckId}_$slide.num', slide.image);
+ if (deckId == null) {
+ throw new ArgumentError.notNull('deckId');
+ }
+ if (slide == null) {
+ throw new ArgumentError.notNull('slide');
+ }
+ return new _RawImageProvider('slide_${deckId}_${slide.num}', slide.image);
}
class _RawImageProvider implements ImageProvider {
@@ -23,7 +32,6 @@
final List<int> imageData;
_RawImageProvider(this.imageKey, this.imageData);
-
Future<ui.Image> loadImage() async {
return await decodeImageFromList(new Uint8List.fromList(imageData));
}